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.
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.
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 | 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
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(())
}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?;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?;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.
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.
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.
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());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.
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.
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.
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/disconnectCurrent 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 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.
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(())
}# 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 15The 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.
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.
| 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(...).
- 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.
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.
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?;| 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.
gvm-protocol provides the transport-agnostic building blocks:
XmlCommand— Builder for GMP XML commands with attributes, child elements, and text contentResponse— Parser for GMP XML responses (status codes, child text extraction, id extraction)XmlReader— Streaming XML completeness detector for framing GMP messages over byte streams
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.
# 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- Rust 1.89+ (MSRV)
- Python 3.10+ with the locked integration requirements (CI uses Python 3.12)
| 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.
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 |
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.
Start with the documents that describe the current implementation:
STATUS.md— implemented command, response, and client surfaceSUPPORT_MATRIX.md— qualified GMP versions and command gatesROADMAP.md— support direction and current follow-up worktyped-execution.md— current typed request/response modelstreaming-responses.md— capability-gated exact-byte response streamingv0.10.0-migration.md— migration from the v0.9 release surfacev0.9.0-migration.md— migration from the v0.8 release surfacev0.8.0-migration.md— migration from the v0.7 release surfacev0.7.0-migration.md— migration from the v0.6 request surfaceagent-code-map.md— code ownership and contributor orientation
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.
AGPL-3.0-or-later — matching the Greenbone ecosystem.
Copyright 2026 Greenbone AG
- python-gvm — Official Python GMP library
- gvmd — Greenbone Vulnerability Manager daemon
- openvas-mcp-server — MCP Server for OpenVAS/Greenbone