Skip to content
greenbonePublic

About

Rust client library for the Greenbone Management Protocol (GMP)

Resources

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Latest commit

 

History

499 Commits

Folders and files

Repository files navigation

rust-gvm

Rust client and protocol ecosystem for the Greenbone Management Protocol (GMP) and Greenbone Vulnerability Manager (gvmd), with type safety, async-first design, version-aware canonical requests, and a programmable mock server for testing.

CI Security

Warning

Technology Preview: This code is provided as a Technology Preview only. All APIs are still subject to change and must not be considered stable.

Tagged release builds publish CycloneDX 1.5 JSON and XML SBOMs generated by cargo-cyclonedx. The JSON artifacts are post-processed to add CC0 document licensing, build lifecycle metadata, and supplier hints before sbomqs scoring and packaging. CPEs are intentionally left untouched unless a reliable package mapping is available.

Note

Releases are managed via the repository's orchestrated release workflow. Pre-releases use the same exact-revision release path with a SemVer pre-release version. This repository does not currently run a scheduled nightly workflow. See RELEASING.md for details.

Overview

rust-gvm provides everything needed to talk to gvmd — from low-level XML framing to high-level typed commands — plus a standalone mock server that speaks the GMP protocol over Unix sockets, TCP, or TLS, enabling integration testing without a real Greenbone instance.

The primary goal is to track current GMP/GVMD behavior directly. Compatibility with python-gvm remains valuable for migration, interoperability, and validation, but it is a secondary compatibility target rather than the boundary of the project.

See docs/ROADMAP.md for the support direction, version policy, compatibility stance, and follow-up work.

Crate Structure

Crate Purpose Status
gvm-protocol Sans-I/O XML framing, command builder, response parser ✅ Implemented
gvm-mock-server Programmable mock GMP server (4 modes, fault injection) ✅ Implemented
gvm-connection Transport layer (Unix socket, TLS, SSH) ✅ Implemented
gvm-gmp Complete typed GMP requests, responses, and capability metadata (22.4–22.8+) ✅ Implemented
gvm-client High-level async client with version negotiation ✅ Implemented
┌─────────────────────────────────┐
│         gvm-client              │  High-level API, version negotiation
├─────────────────────────────────┤
│         gvm-gmp                 │  Canonical requests, codecs, responses
├─────────────────────────────────┤
│       gvm-protocol              │  Sans-I/O XML framing + response parsing
├─────────────────────────────────┤
│      gvm-connection             │  Unix socket / TLS / SSH transports
└─────────────────────────────────┘

  gvm-mock-server  ← Standalone mock for testing any GMP client

Quick Start

Client — Connect to gvmd

The high-level client handles version negotiation automatically and exposes typed convenience methods for every GMP domain:

use gvm_client::GmpClient;
use gvm_connection::{UnixSocketConfig, UnixSocketConnection};
use gvm_gmp::commands::authentication::AuthenticateRequest;
use gvm_gmp::commands::targets::{CreateTargetRequest, GetTargetsRequest};
use gvm_gmp::commands::tasks::{CreateTaskRequest, StartTaskRequest};
use gvm_gmp::{TargetHost, TargetHosts, TargetPortSelection};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Create a transport and connect — auto-negotiates GMP version (22.4–22.8+)
    let conn = UnixSocketConnection::new(UnixSocketConfig::new("/run/gvmd/gvmd.sock"));
    let mut client = GmpClient::connect(conn).await?;
    println!("Connected, GMP version: {}", client.version());

    // 2. Authenticate
    client
        .authenticate(AuthenticateRequest::new("admin", "admin"))
        .await?;

    // 3. Create a target — typed response, no manual XML parsing
    let hosts = TargetHosts::new(["192.168.1.0/24".parse::<TargetHost>()?], [])?;
    let ports = TargetPortSelection::PortRange("T:1-65535".parse()?);
    let target = client
        .create_target(CreateTargetRequest::new("My Target", hosts, ports))
        .await?;
    println!("Created target: {}", target.id);

    // 4. List all targets
    let targets = client.get_targets(GetTargetsRequest::default()).await?;
    for t in &targets.items {
        println!("  {} — {}", t.meta.id, t.meta.name);
    }

    // 5. Create and start a scan task
    let config_id = "daba56c8-73ec-11df-a475-002264764cea".parse().unwrap();
    let scanner_id = "08b69003-5fc2-4037-a479-93b440211c73".parse().unwrap();
    let task = client.create_task(CreateTaskRequest::new(
        "My Scan", config_id, target.id, scanner_id,
    )).await?;
    client.start_task(StartTaskRequest::new(task.id.clone())).await?;
    println!("Started task: {}", task.id);

    client.disconnect().await?;
    Ok(())
}

Canonical filters

Filter operations use complete request values; callers do not combine required arguments with a separate options bag:

use gvm_gmp::commands::filters::{CreateFilterRequest, GetFiltersRequest};
use gvm_gmp::FilterType;

let mut request = CreateFilterRequest::new("Recent tasks");
request.term = Some("rows=10 sort-reverse=modified".into());
request.filter_type = Some(FilterType::Task);
let created = client.create_filter(request).await?;

let filters = client
    .get_filters(GetFiltersRequest {
        details: Some(true),
        alerts: Some(true),
        ..Default::default()
    })
    .await?;

Canonical tags

Tag operations use complete requests and an explicit resource selection:

use gvm_gmp::commands::tags::{CreateTagRequest, GetTagsRequest, TagResources};
use gvm_gmp::EntityType;

let mut resources = TagResources::new(EntityType::Task);
resources.filter = Some("status=Running".into());
let mut request = CreateTagRequest::new("Running tasks", resources);
request.value = Some("triage".into());
let created = client.create_tag(request).await?;

let tags = client
    .get_tags(GetTagsRequest {
        details: Some(true),
        ..Default::default()
    })
    .await?;

Canonical scanners

Scanner operations use complete request values, including gvmd's relay fields and explicit clear semantics:

use gvm_gmp::commands::scanners::{CreateScannerRequest, ModifyScannerRequest};
use gvm_gmp::{ScalarUpdate, ScannerType};

let mut create = CreateScannerRequest::new(
    "Remote scanner",
    "scanner.example",
    9390,
    ScannerType::OpenVasScanner,
);
create.relay_host = Some("relay.example".into());
create.relay_port = Some(9391);
let created = client.create_scanner(create).await?;

let mut modify = ModifyScannerRequest::new(created.id);
modify.credential_id = ScalarUpdate::Clear;
modify.relay_host = Some(String::new());
client.modify_scanner(modify).await?;

Create validates the required name, host, non-zero port, scanner type, and relay shape before transport. Modify distinguishes omission from explicit clearing for credentials, CA certificates, comments, and relays.

Typed schedules

Common schedule recurrence does not require callers to construct or parse iCalendar. Timestamps are accepted as RFC 3339 and normalized to UTC with second precision. Recurrence is anchored at the first run; the separate timezone value and timezone-dependent run timestamps are resolved authoritatively by gvmd.

use gvm_gmp::commands::schedules::{CreateScheduleRequest, GetSchedulesRequest};
use gvm_gmp::{
    ScheduleDefinition, ScheduleInput, ScheduleRecurrence, ScheduleTimestamp,
    ScheduleTimezone,
};

let schedule = ScheduleInput::new(
    ScheduleDefinition {
        first_run: ScheduleTimestamp::parse("2030-01-01T08:00:00+01:00")
            .expect("valid RFC 3339 timestamp"),
        recurrence: ScheduleRecurrence::Weekly,
    },
    ScheduleTimezone::new("Europe/Berlin").expect("non-empty timezone"),
);
let created = client
    .create_schedule(CreateScheduleRequest::from_input("Weekly scan", schedule))
    .await?;

let observed = client
    .get_schedules(GetSchedulesRequest::default())
    .await?;
let schedule = observed.items.iter().find(|item| item.meta.id == created.id);

CreateScheduleRequest::new and ModifyScheduleRequest::new accept raw iCalendar for recurrence rules outside the typed once/hourly/daily/weekly/yearly subset. Raw create requests may omit the timezone and let gvmd use the user's timezone; modify requests must resend the iCalendar payload, while an omitted timezone keeps the existing value. Valid but unsupported timezone or recurrence semantics (including recurrence dates and exclusions) are reported explicitly through the typed observation instead of being treated as one-time schedules. Floating or TZID-qualified iCalendar starts remain observable but are not converted locally; typed first_run_at and next_run_at values use gvmd's normalized response fields.

Validated target hosts

Canonical target create and modify requests use TargetHost and the non-empty TargetHosts aggregate, so invalid individual specifications, fully excluded host sets, and invalid request shapes are rejected before a GMP request is built or sent. Included and excluded hosts are replaced together on modify, matching gvmd. Individual IPv4 and IPv6 addresses, gvmd-supported CIDR networks, address ranges, and ASCII hostnames can be parsed directly. IPv4 components with leading zeroes are normalized before serialization:

use gvm_gmp::{TargetHost, TargetHosts};

let hosts: Vec<TargetHost> = ["192.0.2.1", "10.0.0.0/24", "scanner.example.com"]
    .into_iter()
    .map(str::parse)
    .collect::<Result<_, _>>()?;
let target_hosts = TargetHosts::new(hosts, [])?;
assert!(target_hosts.has_effective_hosts());

This is a source-level change from the earlier Vec<String> fields. Callers should parse external text into TargetHost before constructing CreateTargetRequest or ModifyTargetRequest. IPv4 CIDR prefixes are limited to /1 through /30; IPv6 prefixes are validated independently through /128. Hostname labels intentionally use an ASCII policy; Unicode case-fold lookalikes accepted incidentally by some GLib regex versions are rejected. Canonical identities de-duplicate equivalent IP, network, range, and ASCII hostname spellings while retaining the first wire spelling; a hostname with a trailing dot remains distinct from the same spelling without one, as in gvmd. The aggregate can also detect when exclusions cover every included specification without expanding large networks. CIDR coverage follows gvmd's usable-address rules (excluding the first and last addresses except for IPv6 /127 and /128). gvmd remains authoritative for DNS resolution and deployment policy such as its configured maximum number of IPs per target.

The typed CreateTargetRequest models manual-host creation. Raw GMP callers can still use gvmd's <asset_hosts filter="..."/> target form; stateful mock mode resolves matching host assets and gives that filter precedence over <hosts>, matching gvmd. Its shared asset-filter subset supports quoted values, equality, case-insensitive containment (~ and :), comparisons, sorting, and pagination; malformed or explicitly unsupported predicates receive a client error. Creation also requires a typed TargetPortSelection: either an existing port-list ID or a validated direct range such as "T:22, U:53, T:80-443". This is an intentional one-of restriction in the typed API. Raw GMP permits both elements; gvmd validates the supplied range and gives <port_list> precedence. Existing callers select the ports field when constructing CreateTargetRequest.

Raw API (send/call)

For full control you can use the underlying send() / call() methods directly with a raw request value. Canonical families intentionally do not keep parallel public builders:

// call() raises GvmError::Server on non-2xx; send() returns the raw Response
client
    .call(b"<authenticate><credentials><username>admin</username><password>admin</password></credentials></authenticate>".as_slice())
    .await?;
let response = client.call(b"<get_targets/>".as_slice()).await?;
println!("Raw XML: {} bytes", response.data().len());

Statically associated typed execution

The Technology Preview client exposes semantic request values whose response type is selected at compile time. The completed migration covers version negotiation, authentication, standard and specialized task variants, standard, OCI-image, and web-application target lifecycles, credential/scanner lifecycles, scan-config, policy, and credential-store operations, alerts and schedules, filters, tags, notes, overrides, trashcan recovery, user, group, role, and permission lifecycles, plus irregular report retrieval, drill-down, export, create, import, and delete operations and the complete NVT/SecInfo query surface. Canonical generic asset, host, operating-system asset, and result list/detail queries, plus the GMP 22.8 agent, agent-group, and integration-configuration families use the same execution contract, including binary/base64 support bundles. Generic configuration and port-list/port-range lifecycles and the report-configuration, report-format, and TLS-certificate lifecycles are also fully migrated. NVT/SecInfo discovery uses 19 complete request values and request-by-value facades; current get_info supports CERT-Bund, CPE, CVE, DFN-CERT, NVT, and the feature-gated web_application_vt subtype, while observed vulnerabilities use get_vulns. See the pinned NVT/SecInfo evidence. Configuration lifecycle calls likewise use complete request values. Named creation requires an explicit base; import validates and embeds one exported configuration; metadata modification cannot change usage or clear fields with empty text. Preference list/single reads and the eight scan-config/policy preference and ordered selection mutations are complete request values executed through client.execute; decoded secret values are encoded exactly once and delete remains distinct from explicit empty. Canonical configuration usage is scan or policy, while raw XML remains available for server-defined custom queries. The schema-only sync_config API is not exposed because pinned and current gvmd have no GMP dispatcher for it. See the scan-config/policy evidence. TLS creation accepts original PEM or DER bytes and encodes the GMP carrier exactly once; the lifecycle has no private-key, certificate-replacement, trash, or restore controls. See the pinned gvmd evidence. Read-only system discovery is covered as well, including aggregates, features, feeds, settings, timezones, help, system reports, generic information, preferences, resource names, vulnerabilities, license status, and authentication description. System authentication, license, and wizard mutations plus user-setting list, detail, and modification operations are also complete direct-codec requests. Trashcan cleanup has one canonical restore operation, matching gvmd; the former restore_from_trashcan alias is removed. Confidential administration and setting values retain their diagnostic and wire-trace redaction guarantees:

use gvm_gmp::commands::targets::GetTargetsRequest;

let targets = client
    .execute(GetTargetsRequest::default())
    .await?;

The final convergence audit classifies all 1,039 request-surface rows and all 267 public typed facade helpers. It enforces 264 canonical execute delegates, only three frozen ticket raw paths, zero transitional rows, and the continued absence of unsupported configuration synchronization. See the canonical-request convergence audit and the complete v0.6.0 to v0.7.0 migration guide.

Capability-gated response streaming

GmpClient::execute_stream provides the generic foundation for response types that explicitly opt in through their statically associated response capability. The borrowed response stream yields exact received XML bytes with caller-driven backpressure, validates exactly one frame incrementally, and returns an observed byte count plus small typed completion metadata. Full validation leaves the connection reusable; transport/framing/decoder failure or incomplete drop makes it unusable until reconnect. An AsyncWrite copy adapter and an explicit drain-and-validate completion path are included.

GetNativeReportPageRequest is the first production opt-in. It requires an explicit report ID, pagination-free result filter, 1-based first, positive rows, and named lean/note-detail/override-detail/result-tag choices. It streams one complete unchanged native get_reports_response through the generic execute_stream API while retaining only bounded decoder state and small IDs, status, filter/window, result totals, and the actual number of returned results. Use that returned count—not requested rows—to continue after gvmd caps a page. Buffered execution returns the same small metadata but deliberately does not retain the native XML bytes. See Streaming Large GMP Responses for pagination, memory limits, cancellation, sink-error, tracing, and reuse semantics.

NVT and SecInfo discovery follows the same request-by-value rule:

use gvm_gmp::commands::nvts::GetNvtRequest;
use gvm_gmp::commands::secinfo::{GenericInfoType, GetInfoRequest};

let nvt = client
    .get_nvt(GetNvtRequest::new("1.3.6.1.4.1.25623.1"))
    .await?;
let cve = client
    .get_info(GetInfoRequest::new("CVE-2026-1000", GenericInfoType::Cve))
    .await?;
# let _ = (nvt, cve);
# Ok::<(), Box<dyn std::error::Error>>(())

Asset operations now take complete request values. Generic reads require an AssetType; direct creation requires one IPv4 or IPv6 host name; modification replaces or clears the host comment; and deletion is permanent without an ultimate mode. The redundant asset/host/OS option bags and free builders are removed, as is the unsupported OS-modification helper. Asset OS remains distinct from SecInfo OS. See the migration guide and pinned gvmd evidence.

Result queries also use complete requests over the shared get_results wire root. List requests expose result/task context, inline and saved filters, detail and expansion-detail controls, and optional counts. The detail constructor selects one result and defaults details to true:

use gvm_gmp::commands::results::{GetResultRequest, GetResultsRequest};
use gvm_gmp::EntityId;

let mut list = GetResultsRequest::default();
list.filter_string = Some("task_id=task-1 rows=25 sort-reverse=severity".into());
list.notes_details = Some(true);
let results = client.get_results(list).await?;

let result_id = EntityId::new("result-1")?;
let detail = client
    .get_result(GetResultRequest::new(result_id))
    .await?;
# let _ = (results, detail);
# Ok::<(), Box<dyn std::error::Error>>(())

Root task_id supplies note/override context; it does not select results. Inclusion (notes/overrides) and override application remain filter controls, while the corresponding detail flags only change expansion richness. Counts may be absent. Nested expansion payloads remain available through raw send/call, outside the current ScanResult projection. See the result migration and pinned gvmd evidence. Result NVT references and detailed NVT responses share an optional raw-preserving Epss projection for the independent maximum-severity and maximum-EPSS CVEs. Numeric accessors accept only finite values, and percentiles retain gvmd's native percentage unit; see the EPSS evidence.

Report configurations use six complete lifecycle requests. Direct creation uses the gvmd-supported format reference, and parameter assignments distinguish explicit empty values from omission and reset:

use gvm_gmp::commands::report_configs::{
    CreateReportConfigRequest, ReportConfigParam, ReportConfigParamValue,
};
use gvm_gmp::EntityId;

let mut request = CreateReportConfigRequest::new(
    "PDF settings",
    EntityId::new("report-format-1")?,
);
request.comment = Some(String::new()); // explicit empty comment
request.params.push(ReportConfigParam {
    name: "Graph Type".into(),
    value: ReportConfigParamValue::UseDefault,
});
let created = client.create_report_config(request).await?;
# let _ = created;
# Ok::<(), Box<dyn std::error::Error>>(())

Queries expose ID, inline/saved filters, trash, details, and pagination bypass; first, rows (including rows=-1), and sorting stay in caller-authored filter text. Clone accepts only a name override. There is no report-config import, preference/usage-type wrapper, or modify-format replacement. The typed response intentionally omits parameter observations; use raw send/call when the complete response XML is required. See the migration guide and pinned gvmd evidence.

Report formats use seven explicit request values for list, detail, import, clone, modify, delete, and verify. Direct name-only creation is removed because gvmd supports creation only by importing an exported response envelope or cloning an existing format. Imports preserve the validated envelope bytes and return an authoritative created ID; modification supports metadata plus one decoded parameter value, which is base64-encoded exactly once:

use gvm_gmp::commands::report_formats::{
    ModifyReportFormatRequest, ReportFormatParamUpdate,
};
use gvm_gmp::EntityId;

let mut request = ModifyReportFormatRequest::new(EntityId::new("format-1")?);
request.summary = Some(String::new()); // paired empty element clears summary
request.param = Some(ReportFormatParamUpdate {
    name: "Label".into(),
    value: Some("custom".into()),
});
client.modify_report_format(request).await?;
# Ok::<(), Box<dyn std::error::Error>>(())

Omitting a parameter preserves all parameters; a missing or empty value clears one parameter and does not reset it to a default. Compound metadata/parameter updates can partially commit upstream if parameter validation later fails. Clone naming, changed trash IDs, alert-use deletion guards, and verification completion versus observed trust are documented in the migration guide and pinned gvmd evidence. The typed response is intentionally smaller than a complete export; raw send/call remains available.

The standard target family is the canonical reference slice: its requests own their complete input and encoding, and its redundant options types and free builders are removed. OCI-image and web-application targets follow the same contract on GMP 22.8:

use gvm_gmp::commands::oci_image_targets::CreateOciImageTargetRequest;

let request = CreateOciImageTargetRequest::new(
    "production image",
    vec!["registry.example/app:stable".into()],
);
let target = client.create_oci_image_target(request).await?;

Credential requests follow the same single-value contract; secret-bearing fields live on the request and are redacted from diagnostics and wire traces:

use gvm_gmp::commands::credentials::CreateCredentialRequest;
use gvm_gmp::CredentialType;

let mut request = CreateCredentialRequest::new("scanner login");
request.credential_type = Some(CredentialType::UsernamePassword);
request.login = Some("scanner".into());
request.password = Some(password);
let credential = client.create_credential(request).await?;

Agent groups also use complete canonical requests on GMP 22.8. Their six list/detail/create/clone/modify/delete helpers accept the same request values as execute; filters, scheduler data, repeated agent IDs, and mutation fields no longer require separate options values or free builders. Agent membership updates explicitly distinguish omission from non-empty replacement. Although the shared type can represent clear intent, current gvmd treats <agents/> as preserve, so the typed request rejects Clear before transport instead of claiming a clear that the server does not perform. Web-application target excluded URLs use the same shared update type and do support all three states; their empty element maps to gvmd's SQL NULL clear operation.

Alerts use complete request values for list, detail, create, clone, modify, delete, test, and report-trigger operations. Required create fields are typed, and nested alert data is redacted from diagnostics and wire traces.

Users and groups use complete request values for their list, detail, create, clone, modify, and delete lifecycles. User relationship updates explicitly preserve, replace, or clear roles and groups, while ModifyUserRequest carries the final host-access value required by gvmd. Password fields remain redacted from request diagnostics and wire traces:

use gvm_gmp::commands::users::{
    CreateUserRequest, ModifyUserRequest, UserHostAccess,
};

let mut create = CreateUserRequest::new("operator");
create.password = Some(password);
create.host_access = Some(UserHostAccess::deny("192.0.2.0/24"));
let created = client.create_user(create).await?;

let mut modify =
    ModifyUserRequest::new(created.id, UserHostAccess::allow(""));
modify.comment = Some("production operator".into());
client.modify_user(modify).await?;
# Ok::<(), Box<dyn std::error::Error>>(())

All other supported non-ticket families follow the same canonical request contract; ticket support remains deliberately frozen. Raw send and call remain the supported low-level escape hatch. See the downstream migration notes for API selection, compatibility, and release adoption, and the contributor-oriented typed request/response execution guide for the raw escape hatch and command-authoring guidance.

Version-aware client

Use GmpVersioned when you need to branch on the server's GMP version:

use gvm_client::GmpVersioned;
use gvm_connection::{UnixSocketConfig, UnixSocketConnection};

let conn = UnixSocketConnection::new(UnixSocketConfig::new("/run/gvmd/gvmd.sock"));
let mut client = GmpVersioned::connect(conn).await?;

match &client {
    GmpVersioned::V225(_) => println!("GMP 22.5"),
    GmpVersioned::V226(_) => println!("GMP 22.6"),
    _ => println!("Other version: {}", client.version()),
}

// All versions share the same send/call API.
client
    .call(b"<authenticate><credentials><username>admin</username><password>admin</password></credentials></authenticate>".as_slice())
    .await?;

GmpClient::command_support and GmpVersioned::command_support distinguish commands that are supported, require explicit XML-help discovery, require a newer GMP version, were not advertised after discovery, or are unknown to the library. The same classification drives the pre-send execution gate. Unknown names remain available through raw send/call for custom commands.

SSH transport

Connect to a remote gvmd over SSH. Existing callers use OpenSSH stream-local forwarding to the remote gvmd Unix socket by default:

SshConnection is available when the direct gvm-connection dependency enables its ssh feature. Transport features are owned by gvm-connection; they are not features on gvm-client.

use gvm_client::GmpClient;
use gvm_connection::{SshConfig, SshAuth, SshConnection};

let config = SshConfig::new("scanner.example.com", "gvm", SshAuth::Agent)
    .with_port(22)
    .with_remote_socket("/run/gvmd/gvmd.sock");
let conn = SshConnection::new(config);

let mut client = GmpClient::connect(conn).await?;
// Same API as Unix socket — connect/call/disconnect

Current Greenbone appliances and SSH accounts configured with a server-side forced command use a standard session channel plus an exec request instead. Select that topology explicitly; an empty command matches the current GOS and python-gvm connection behavior:

use gvm_connection::{SshAuth, SshChannelMode, SshConfig, SshConnection};

let config = SshConfig::new("appliance.example.com", "gmp", SshAuth::Password(
    std::env::var("GMP_SSH_PASSWORD")?,
));
let conn = SshConnection::new(config).with_channel_mode(SshChannelMode::Exec {
    command: String::new(),
});

The exec command is sent unchanged as an opaque SSH request value. rust-gvm does not run a local shell, interpolate arguments, or add quoting. The SSH server controls interpretation and may replace the requested value with ForceCommand or an authorized_keys command="..." restriction. Shell requests are a different SSH topology and are not implied by exec mode.

For a dedicated generic OpenSSH account, a restricted key can bridge standard input/output to gvmd while disabling unrelated SSH capabilities, for example:

restrict,command="socat STDIO UNIX-CONNECT:/run/gvmd/gvmd.sock" ssh-ed25519 AAAA...

This example requires socat on the server. Keep the account and key dedicated to GMP, restrict filesystem and process privileges around the forced command, retain GMP authentication, and verify the SSH host key. The SSH administrator—not rust-gvm—owns command parsing, executable selection, and Unix-socket access.

SSH verifies the server key against ~/.ssh/known_hosts by default, including the configured port. Use SshHostKeyPolicy::KnownHostsFile for a nonstandard file or SshHostKeyPolicy::Fingerprint for an explicitly pinned SHA-256 fingerprint. Callers that need python-gvm's auto_accept_host behavior can opt in to SshHostKeyPolicy::TrustOnFirstUse or TrustOnFirstUseFile: an unknown key is persisted atomically, while repeat connections require that same key. TOFU does not authenticate the first connection, so that connection remains vulnerable to a man-in-the-middle attack. SshHostKeyPolicy::AcceptAll never pins a key, disables verification on every connection, and is only appropriate for tests or an independently authenticated transport. This secure default is a behavior change for callers that previously relied on implicit host-key acceptance; choose one of the explicit trust policies when migrating existing deployments.

TLS transport

TLS verifies the server certificate and DNS/IP subject name by default. Add a private CA for gvmd deployments that do not use a platform-trusted certificate:

TlsConnection is available when the direct gvm-connection dependency enables its tls feature.

use gvm_client::GmpClient;
use gvm_connection::{TlsClientIdentity, TlsConfig, TlsConnection};

let identity = TlsClientIdentity::from_files("client.pem", "client-key.pem")?;
let config = TlsConfig::new("gvmd.example.com")
    .with_root_certificate_file("gvmd-ca.pem")?
    .with_client_identity(identity);
let conn = TlsConnection::new(config);

let mut client = GmpClient::connect(conn).await?;

Client certificates are optional. Disabling platform roots never disables verification; configure at least one custom root when platform roots are off. For buffered reads on Unix, TLS, and SSH, the configured timeout is a total deadline for one complete XML response. Capability-gated streaming instead uses that value as the default idle timeout for each transport read; callers can independently set a per-stream total deadline with StreamOptions.

Mock Server (library usage)

use gvm_mock_server::{MockGmpServer, ServerMode, GmpVersion};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let server = MockGmpServer::builder()
        .mode(ServerMode::Stateful)
        .version(GmpVersion::V22_7)
        .credentials("admin", "admin")
        .unix_socket_auto()
        .build()
        .await?;

    println!("Mock GMP server listening at: {:?}", server.socket_path());
    // Connect with any GMP client (rust-gvm, python-gvm, etc.)

    server.shutdown().await;
    Ok(())
}

Mock Server (standalone binary)

# Start a stateful mock server on a Unix socket
gvm-mock-server --mode stateful --version 22.7 --socket /tmp/gvmd.sock

# Or on TCP
gvm-mock-server --mode stateful --version 22.7 --tcp 127.0.0.1:9390

# Override the default 64 MiB per-request XML limit
gvm-mock-server --mode echo --tcp 127.0.0.1:9390 --max-request-bytes 1048576

# Tune concurrent sessions and the deadline for completing one XML request
gvm-mock-server --mode echo --tcp 127.0.0.1:9390 \
  --max-sessions 32 --request-timeout-seconds 15

The mock library and CLI default to GMP 22.7. Select 22.8 explicitly when testing agent, container-scanning, or other feature-enabled 22.8 surfaces.

The request-size limit applies independently to each XML command. XML nesting is always limited to 256 elements, including when the byte limit is disabled. The standalone server also limits active sessions to 64 and closes incomplete requests after 30 seconds by default. Non-loopback TCP/TLS listeners require --allow-non-loopback so deterministic test credentials are not exposed by accident.

Then connect with python-gvm:

from gvm.connections import UnixSocketConnection
from gvm.protocols.gmp import GMP
from gvm.transforms import EtreeCheckCommandTransform

conn = UnixSocketConnection(path="/tmp/gvmd.sock")
with GMP(connection=conn, transform=EtreeCheckCommandTransform()) as gmp:
    gmp.authenticate("admin", "admin")
    targets = gmp.get_targets()
    print(targets)

Stateful task creation requires an existing target. If omitted, the config and scanner references use the mock's seeded Full and fast config (daba56c8-73ec-11df-a475-002264764cea) and OpenVAS scanner (08b69003-5fc2-4037-a479-93b440211c73). Explicit target, config, scanner, and imported-report task IDs must resolve to the expected resource type. Referenced resources cannot be deleted while a live task uses them.

Task actions are synchronous mock transitions; they do not run a scanner. start_task creates one linked report, stop_task marks both resources Stopped, and resume_task continues that same report.

Mock Server

The mock server is the most developed component. It's designed to be a drop-in test server for any GMP client library — Rust, Python, Go, or shell scripts.

Server Modes

Mode Description
Echo Returns well-formed <command_response status="200"/> for any recognized command
Fixture Returns pre-built realistic GMP XML from the built-in fixture library
Stateful Bounded in-memory resource behavior with authentication, task lifecycle, and resource relationships; see the support matrix for exact qualifications
Scenario Library-only scripted command→response playback with strict or lenient matching

The standalone CLI accepts echo, fixture, and stateful; configure scenario mode through MockGmpServer::builder().scenario(...).

Features

  • 4 server modes — echo, fixture, bounded stateful behavior, and library-only scenario playback
  • Unix socket, TCP, and feature-gated TLS listeners — TLS uses a generated certificate and can optionally require a trusted client certificate; the default-feature release image exposes Unix/TCP only
  • Per-session authentication — supports python-gvm's two-connection flow (version probe + auth session)
  • Bounded session lifecycle — shared session/channel capacity, total incomplete-request deadlines, authentication cleanup, and draining shutdown
  • Task lifecycle — deterministic New → Running → Stopped transitions with linked report resumption
  • Fault injection — disconnect, delay, malformed XML, error codes, truncated responses
  • Scenario playback — deterministic scripted sequences for regression tests
  • Bounded command history — inspect what the server received after tests; the oldest records are evicted after 1,024 entries or 16 MiB of raw XML, with retained/dropped counters available through command_history_stats()
  • Pre-seeding — populate the store before tests via builder API
  • Template substitution — {{uuid}}, {{now}}, {{version}} in fixture responses
  • Resource filtering — basic GMP filter string support (name=foo status=Running)
  • gvmd-conformant direct-host assets — canonical host/OS responses plus stateful direct-host create/get/modify/delete; historical flat asset inputs require an explicit compatibility profile. Report-import creation and report-based bulk deletion are outside this stateful handler's scope.
  • bounded stateful result conformance — list/ID selection, task/report relationships and effective rendering context, saved/inline filter precedence, deterministic pagination and ID-tied sorting, task-restricted note/override expansions, and effective seeded override filtering, counts, and rendering. Unsupported or malformed filter terms fail explicitly; this is not a full gvmd filter, permission, CVSS, or override engine.
  • bounded stateful report-format conformance — opaque import and inert files, cloning, one-parameter mutation, list/detail expansions, trash/delete, and deterministic injected verification outcomes. It is not a GPG, filesystem, complete ACL/filter, or live-gvmd implementation.

Validated Against

The mock server is validated against python-gvm in CI, exercising Unix-socket target/task/host/note flows plus anonymous TLS and mutual-TLS reconnect/authentication. This protects migration and client interoperability while rust-gvm continues to model GMP/GVMD behavior directly; it is not a real-gvmd conformance claim.

Connection Crate

gvm-connection provides async transport implementations behind the GvmConnection trait:

use gvm_connection::{GvmConnection, UnixSocketConfig, UnixSocketConnection};

let config = UnixSocketConfig::new("/run/gvmd/gvmd.sock");
let mut conn = UnixSocketConnection::new(config);

conn.connect().await?;
conn.send(b"<get_version/>").await?;
let response_bytes = conn.read().await?;  // Uses XmlReader for frame detection
conn.disconnect().await?;

Transports

Transport Status Feature Flag
Unix socket ✅ Implemented unix (default)
SSH tunnel ✅ Implemented ssh
TLS over TCP ✅ Implemented tls

All transports support the python-gvm reconnect pattern (connect → get_version → disconnect → reconnect → authenticate → commands) and implement the GvmConnection trait, so they're interchangeable with GmpClient.

Protocol Crate

gvm-protocol provides the transport-agnostic building blocks:

  • XmlCommand — Builder for GMP XML commands with attributes, child elements, and text content
  • Response — Parser for GMP XML responses (status codes, child text extraction, id extraction)
  • XmlReader — Streaming XML completeness detector for framing GMP messages over byte streams

Implementation Status

See docs/STATUS.md for detailed implementation status of each crate and GMP command coverage.

See docs/ROADMAP.md for current support goals, known protocol coverage gaps, and compatibility expectations.

See docs/COVERAGE.md for the protected Rust workspace and patch-coverage policy.

Building

# Build the default feature set
cargo build --workspace

# Check every transport and crate feature
cargo check --workspace --all-features

# Run the default workspace tests
cargo test --workspace

# Run python-gvm Unix, TLS, and mTLS integration tests
make test-integration

# Build the mock server binary
cargo build --release -p gvm-mock-server

Requirements

  • Rust 1.89+ (MSRV)
  • Python 3.10+ with the locked integration requirements (CI uses Python 3.12)

CI/CD

Workflow Trigger What it does
CI Relevant pushes and pull requests to main Format, clippy, default/all-feature tests, docs, deny, coverage, MSRV, python-gvm integration
Security Relevant pushes and pull requests to main; weekly schedule Audit, vetting, unused-dependency checks, SBOM quality, unsafe-code gates, and Semgrep
OpenSSF Scorecard main pushes, weekly schedule, or manual dispatch; public repositories only Supply-chain posture report and SARIF upload
Journal PR Automation Eligible journal/* PRs to journal-main Restricts changed paths and automates the journal-only review flow
Orchestrated Release Manual dispatch from the exact protected main head Qualifies version/lockfile/notes, creates the tag/release, and waits for the exact-SHA release run
Release v* tag push Locked all-feature tests, five binary builds, multi-platform GHCR image, SBOM bundle, checksums, attestations, and publication verification

After a successful test job on a main push, CI dispatches the external E2E workflow with rust-gvm-ref set to the exact ${{ github.sha }}. Pull requests do not trigger that external workflow.

Pre-built Binaries

Download tagged artifacts from GitHub Releases. There is no current rolling-nightly publication workflow.

Platform Binary
Linux x86_64 (glibc) gvm-mock-server-linux-amd64.tar.gz
Linux x86_64 (musl, static) gvm-mock-server-linux-amd64-musl.tar.gz
Linux ARM64 gvm-mock-server-linux-arm64.tar.gz
macOS x86_64 gvm-mock-server-macos-amd64.tar.gz
macOS ARM64 gvm-mock-server-macos-arm64.tar.gz

GHCR Image

Tagged releases also publish a multi-platform container image at ghcr.io/greenbone-hive/gvm-mock-server:<v-prefixed-tag> for Linux amd64 and arm64. The workflow publishes only the immutable release tag, not latest.

See docs/mock-server-consumption.md for downstream CI usage guidance.

Documentation

Start with the documents that describe the current implementation:

Forward-looking specifications that are still active remain in spec/. Completed plans, implementation prompts, and superseded design snapshots are retained under docs/archive/ for historical context; they are not current API or contributor guidance.

License

AGPL-3.0-or-later — matching the Greenbone ecosystem.

Copyright 2026 Greenbone AG

Related Projects

About

Rust client library for the Greenbone Management Protocol (GMP)

Resources

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages