Every CLI error prints sfos-rs: <message> to stderr and exits 1.
This page lists each message, what actually went wrong, and the fix.
| Message | Cause | Fix |
|---|---|---|
<path>: <reason> |
the config file could not be read or parsed (load failure: missing file, not XML, truncated export) |
check the path; the file must be an Entities.xml backup or an XML API <Response> body. Try parse <file> to isolate. A single unmodelled entity no longer fails the load — see partial parse below; a hard error here means the file itself is unreadable or not XML. |
specify --referencing <object>, or both --from <zone> and --to <zone> |
search called with no selector, or only one of --from/--to |
pick one mode: --referencing NAME, or both --from Z --to Z |
unknown --proto '<x>' (use tcp|udp|icmp) |
--proto was something else |
only tcp, udp, icmp are modelled |
invalid --src IP '<x>' / invalid --dst IP / invalid --to IP '<x>' |
the value didn't parse as an IPv4/IPv6 address | site-path takes IPs only; path/explain --to also accept an IPHost object name |
--to '<x>' is not an IP and not a resolvable host object |
explain/path tried to resolve --to as an IPHost name and failed |
use the exact object Name from dump --hosts, or pass the IP directly |
could not infer a zone for --src <ip> (no interface covers it) |
explain --src could not map the IP to a zone — no interface subnet contains it |
pass --from <zone> explicitly, or check the config actually contains interface addressing |
no zones to evaluate — config has no zones; pass --from or --src |
explain over a config with zero zones and no --from/--src given |
verify the export contains Zone entities, or name zones explicitly |
| Message | Cause | Fix |
|---|---|---|
provide --password or set SFOS_PASSWORD |
neither the flag nor the env var is set | export SFOS_PASSWORD=… (preferred) or --password |
--user is required with --host |
apply --host without --user |
add --user admin |
specify --live <file> for an offline plan, or --host <fw> to plan against a live firewall |
apply with neither a live file nor a host |
choose one source of truth for the "live" side |
--commit requires --host (cannot write changes to a --live file) |
apply --live <file> --commit |
--live plans are offline-only by design; to write, point at the firewall with --host |
These surface through any live command (fetch, get, export, apply)
and through library calls.
The request never produced an HTTP response. The <detail> is the underlying
reqwest error; common cases:
- certificate errors (
invalid peer certificate,self-signed, …) — the firewall is using its default self-signed cert and you didn't pass--insecure(SDK:verify_certs=false) - connection refused / timed out (30 s limit) — wrong
--port(API is on the webadmin port, default 4444), the admin service isn't listening on that interface, or a firewall rule blocks you. If your IP isn't in the API allowlist some configurations drop the connection outright — see playbooks.md - DNS errors — bad
--host
The server answered, but not 2xx. A 404 usually means the endpoint path doesn't exist on the target (is this actually an SFOS webadmin port?); 5xx means the admin service errored.
The firewall's <Login> status contained the literal text
Authentication Failure. Causes, most common first:
- wrong username/password
- the XML API is not enabled, or your source IP is not in the API allowlist (Backup & firmware → API) — SFOS rejects the login rather than admitting the API is off
- the account's admin profile doesn't permit API access
A Set/Remove was accepted at the HTTP layer but the firewall rejected the
operation: its <Status code="NNN"> was outside 200–299. The code and the
human-readable text in the response come from the firewall itself — the SDK
deliberately does not maintain its own table of Sophos status codes. To see
the firewall's full explanation, replay the read side with
get <Tag> --raw, or run apply without --commit and inspect the exact
<Set> body being sent. Typical causes: referencing an object that doesn't
exist yet (create dependencies first — zones/hosts/services before rules),
name collisions, or a field invalid for your firmware version.
A response with no <Status> element at all is treated as success
(benign), matching observed SFOS behaviour for some read paths.
The response body wasn't valid XML at the document level (truncated, wrong
content type, an HTML error page, …). A single unmodelled entity inside an
otherwise-valid response no longer trips this — it's skipped (see
partial parse). If get <Tag> --raw shows
valid XML but a tag you need is missing from the parsed output, its shape isn't
modelled yet — use --raw/JSON output, and open an issue with the (redacted) XML.
Per-item failures do not abort the run:
! ADD web-host failed: API error: status 502
applied 3 change(s), 1 failed
Exit code is 1 if anything failed. The applied changes stay applied —
there is no rollback. Re-running apply is the recovery path: the plan is
recomputed against the new live state, so already-applied items drop out.
When an offline command loads a config, a clean document is parsed whole. If the file contains a top-level entity whose shape the typed model can't take (an element repeated where a scalar was expected, a field missing, a firmware-specific shape we don't model yet), the loader skips that one element and keeps going rather than aborting. You'll see a note on stderr:
sfos-rs: note: skipped 1 unmodelled entity (VPNIPSecConnection ×1); analysis continues on the rest
sfos-rs: first: <VPNIPSecConnection> — <detail>
The analysis then runs on everything that did parse, and exit codes reflect
the findings, not the skip. This is by design — a real export exercises far
more of the schema than the curated fixtures, and one odd entity shouldn't
sink the whole report. If a tag you care about is being skipped, capture it
live with get <Tag> --raw and open an issue with the (redacted) XML so the
model can be extended.
Note:
XML parse erroronly surfaces now when the file as a whole isn't valid XML (truncated, wrong encoding, not a<Configuration>/<Response>document). Per-entity modelling gaps become skips, not hard failures.
exported 58 entities (8 unavailable) on stderr is normal: entities that
don't exist on a given model/firmware (e.g. wireless on a virtual appliance)
fail individually. With --out-dir failed entities are skipped; in combined
JSON output they appear as {"error": "<message>"} values, so you can grep
for what your box doesn't serve.
| Command | exits 1 when |
|---|---|
| any | an error above occurred |
check, s2s |
a HIGH or CRIT finding exists |
trace, path, site-path |
the verdict is BLOCKED |
apply --commit |
at least one change failed |
If you're migrating from sophosfirewall-python:
| Python exception | sfos-rs equivalent |
|---|---|
SophosFirewallAuthFailure |
SdkError::Auth |
SophosFirewallAPIError |
SdkError::Api { code } |
SophosFirewallZeroRecords |
no error — an empty SophosConfig / empty JSON. Zero records is data, not an exception; test for emptiness explicitly. |