diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc27d04..1b7a225 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: with: version: "25" - name: Check C++ Format - run: /opt/wasi-sdk/bin/clang-format --dry-run --Werror src/**/*.h src/**/*.cpp + run: /opt/wasi-sdk/bin/clang-format --dry-run --Werror include/**/*.h src/**/*.h src/**/*.cpp test: runs-on: ubuntu-latest diff --git a/Cargo.lock b/Cargo.lock index 4ea9643..457fa64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,9 +244,9 @@ dependencies = [ [[package]] name = "fastly" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9506877e1713a00a6a676191c29cc449aa60129da123d81245b203a4f6734dd3" +checksum = "ac590af69cdea42ebbbaa566d0e603c6c0d7d6f53a507fe82cea65260419ab88" dependencies = [ "anyhow", "bytes", @@ -254,7 +254,7 @@ dependencies = [ "elsa", "fastly-macros", "fastly-shared", - "fastly-sys 0.11.5", + "fastly-sys 0.11.9", "http", "itertools", "lazy_static", @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "fastly-macros" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3331e33178c193355093de80c370b3d65ed7a61631a38ad32bf84bedec6ca9e8" +checksum = "b012bd5c924ede9a1363ad29a232c4e95c9eb520a124979ad06043a6e44025dc" dependencies = [ "proc-macro2", "quote", @@ -283,9 +283,9 @@ dependencies = [ [[package]] name = "fastly-shared" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80cbb4ef1d9c7f4321179814dd51b39f2a91f0993a7303396bb34581ae4f5daa" +checksum = "fe8aaf17b8c0b689ce8370052e129c7722f3bd9c5ca27790db7624cf64b8c9b1" dependencies = [ "bitflags 1.3.2", "http", @@ -310,14 +310,14 @@ dependencies = [ [[package]] name = "fastly-sys" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583231411fcb7ba818ca7126b2e97038efc04d29950902b3cd11b33741303d80" +checksum = "a784af8ed4e5f3d32aac54f687b6a2dd844af304390d3bc70d50cbe6a772c1a7" dependencies = [ "bitflags 1.3.2", "fastly-shared", - "wasi", - "wit-bindgen-rt 0.42.1", + "wasip2", + "wit-bindgen", ] [[package]] @@ -544,9 +544,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "log-fastly" -version = "0.11.5" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5cb1cc40a2a1daaa295854ae3c9e0ead63c3f7ad267e6979aaee6ac9635b9c" +checksum = "c67b1d4ff825b027926a385cea5a9f43d0b5a900030d48736833a552120e2b59" dependencies = [ "fastly", "log", @@ -934,12 +934,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt 0.39.0", + "wit-bindgen", ] [[package]] @@ -1025,19 +1025,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] - -[[package]] -name = "wit-bindgen-rt" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051105bab12bc78e161f8dfb3596e772dd6a01ebf9c4840988e00347e744966a" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" dependencies = [ "bitflags 2.9.1", ] diff --git a/Cargo.toml b/Cargo.toml index e260689..7d7c521 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,11 +6,11 @@ edition = "2024" [dependencies] cxx = { version = "1.0.158", features = ["c++17"] } -fastly = "0.11.4" -fastly-shared = "0.11.5" +fastly = "0.11.9" +fastly-shared = "0.11.9" http = "1.3.1" log = "0.4.27" -log-fastly = "0.11.5" +log-fastly = "0.11.9" thiserror = "2.0.12" esi = "0.6.1" quick-xml = "0.38.3" diff --git a/examples/ngwaf_inspect.cpp b/examples/ngwaf_inspect.cpp new file mode 100644 index 0000000..7d2cd0a --- /dev/null +++ b/examples/ngwaf_inspect.cpp @@ -0,0 +1,26 @@ +//! @example ngwaf_inspect.cpp +#include "fastly/sdk.h" + +int main() { + fastly::log::init_simple("logs"); + auto req{fastly::Request::from_client()}; + + auto ires{fastly::security::inspect( + req, + fastly::security::InspectConfig().with_corp("my_corp").with_workspace( + "my_workspace"))}; + auto verdict{ires->verdict()}; + + fastly::Body body{"NGWAF Verdict: "}; + if (verdict == fastly::security::InspectVerdict::Allow) { + body << "Allow"; + } else if (verdict == fastly::security::InspectVerdict::Block) { + body << "Block"; + } else if (verdict == fastly::security::InspectVerdict::Unauthorized) { + body << "Unauthorized"; + } else if (verdict == fastly::security::InspectVerdict::Other) { + body << *ires->unrecognized_verdict_info() << " (Other)"; + } + + fastly::Response::from_body(std::move(body)).send_to_client(); +} diff --git a/include/fastly/backend.h b/include/fastly/backend.h index 67a00e3..f506c65 100644 --- a/include/fastly/backend.h +++ b/include/fastly/backend.h @@ -1,10 +1,10 @@ #ifndef FASTLY_BACKEND_H #define FASTLY_BACKEND_H +#include #include #include #include -#include #include #include diff --git a/include/fastly/http/status_code.h b/include/fastly/http/status_code.h index d5c548e..6537df5 100644 --- a/include/fastly/http/status_code.h +++ b/include/fastly/http/status_code.h @@ -1,11 +1,11 @@ #ifndef FASTLY_HTTP_STATUS_CODE_H #define FASTLY_HTTP_STATUS_CODE_H +#include +#include #include #include #include -#include -#include #include #include diff --git a/include/fastly/security.h b/include/fastly/security.h new file mode 100644 index 0000000..f021503 --- /dev/null +++ b/include/fastly/security.h @@ -0,0 +1,110 @@ +#ifndef FASTLY_SECURITY_H +#define FASTLY_SECURITY_H + +#include +#include +#include +#include +#include + +namespace fastly::security { +/// Configuration for inspecting a `Request` using Security. +class InspectConfig { +public: + /// Create a new default `InspectConfig` + InspectConfig() = default; + + /// Specify an explicity client IP address to inspect. + /// By default, inspect will use the IP address that made the request to the + /// running Compute service, but you may want to use a different IP when + /// service chaining or if requests are proxied from outside of Fastly’s + /// network. + InspectConfig with_client_ip(std::string ip) && { + this->client_ip_ = std::move(ip); + return std::move(*this); + } + + /// Set a corp name for the configuration. + InspectConfig with_corp(std::string name) && { + this->corp_ = std::move(name); + return std::move(*this); + } + + /// Set a workspace name for the configuration. + InspectConfig with_workspace(std::string name) && { + this->workspace_ = std::move(name); + return std::move(*this); + } + + /// Set a buffer size for the response. + InspectConfig with_buffer_size(std::size_t size) && { + this->buffer_size_ = size; + return std::move(*this); + } + + const std::optional &client_ip() const { return client_ip_; } + const std::optional &corp() const { return corp_; } + const std::optional &workspace() const { return workspace_; } + const std::optional &buffer_size() const { return buffer_size_; } + +private: + std::optional client_ip_; + std::optional corp_; + std::optional workspace_; + std::optional buffer_size_; +}; +using fastly::sys::security::InspectErrorCode; +using fastly::sys::security::InspectVerdict; +class InspectError { +public: + InspectError(fastly::sys::security::InspectError *e) + : err_(rust::Box::from_raw(e)) {}; + InspectError(rust::Box e) + : err_(std::move(e)) {}; + InspectErrorCode error_code(); + std::string error_msg(); + /// When getting `InspectErrorCode::BufferSizeError`, this can be used to get + /// the required size of the buffer before re-attempting the call. + std::optional required_buffer_size(); + +private: + rust::Box err_; +}; + +/// Results of asking Security to inspect a `Request` +class InspectResponse { + friend detail::AccessBridgeInternals; + +public: + /// Security status code. + std::int16_t status() const; + /// A redirect URL returned from Security + std::optional redirect_url() const; + /// Tags returned by Security + std::vector tags() const; + /// Get Security's verdict on how to handle this request. + InspectVerdict verdict() const; + /// Get additional information for verdicts where `this->verdict()` is + /// `Other`. + std::optional unrecognized_verdict_info() const; + /// How long Security spent determining its verdict. + std::chrono::milliseconds decision_ms() const; + /// A redirect URI returned by Security. + bool is_redirect() const; + /// Convert a redirect URI returned by Security into a `Response`. + std::optional into_redirect(); + +private: + rust::Box ir_; + InspectResponse(rust::Box ir) + : ir_(std::move(ir)) {}; +}; + +/// Inspect a `Request` using the [Fastly Next-Gen +/// WAF](https://docs.fastly.com/en/ngwaf/). +tl::expected +inspect(fastly::http::Request &request, InspectConfig config); + +} // namespace fastly::security + +#endif diff --git a/src/cpp/http/http.h b/src/cpp/http/http.h index f24c494..fa4fbd4 100644 --- a/src/cpp/http/http.h +++ b/src/cpp/http/http.h @@ -6,6 +6,6 @@ namespace fastly::http { using fastly::sys::http::Method; using fastly::sys::http::Version; -} +} // namespace fastly::http #endif diff --git a/src/cpp/http/response.cpp b/src/cpp/http/response.cpp index d2e9471..e6a8456 100644 --- a/src/cpp/http/response.cpp +++ b/src/cpp/http/response.cpp @@ -168,8 +168,8 @@ fastly::expected Response::with_header(std::string_view name, }); } -fastly::expected Response::with_set_header(std::string_view name, - std::string_view value) && { +fastly::expected +Response::with_set_header(std::string_view name, std::string_view value) && { return this->set_header(name, value).map([this]() { return std::move(*this); }); @@ -180,12 +180,13 @@ Response::get_header(std::string_view name) { std::vector value; bool is_sensitive{false}; fastly::sys::error::FastlyError *err; - bool has_header{ - this->res->get_header(static_cast(name), value, is_sensitive, err)}; + bool has_header{this->res->get_header(static_cast(name), value, + is_sensitive, err)}; if (err != nullptr) { return fastly::unexpected(err); } else if (has_header) { - return std::optional(std::in_place, std::string(value.begin(), value.end()), is_sensitive); + return std::optional( + std::in_place, std::string(value.begin(), value.end()), is_sensitive); } else { return std::nullopt; } @@ -204,16 +205,13 @@ Response::get_header_all(std::string_view name) { } } -fastly::expected -Response::get_headers() { +fastly::expected Response::get_headers() { fastly::sys::http::HeadersIter *out; this->res->get_headers(out); - return HeadersRange( - rust::Box::from_raw(out)); + return HeadersRange(rust::Box::from_raw(out)); } -fastly::expected -Response::get_header_names() { +fastly::expected Response::get_header_names() { fastly::sys::http::HeaderNamesIter *out; this->res->get_header_names(out); return HeaderNamesRange( @@ -344,13 +342,9 @@ Response::get_stale_while_revalidate() { } } -Version Response::get_version() { - return this->res->get_version(); -} +Version Response::get_version() { return this->res->get_version(); } -void Response::set_version(Version version) { - this->res->set_version(version); -} +void Response::set_version(Version version) { this->res->set_version(version); } Response Response::with_version(Version version) && { this->set_version(version); diff --git a/src/cpp/security.cpp b/src/cpp/security.cpp new file mode 100644 index 0000000..9b54090 --- /dev/null +++ b/src/cpp/security.cpp @@ -0,0 +1,80 @@ +#include "util.h" +#include +#include + +namespace fastly::security { +InspectErrorCode InspectError::error_code() { return err_->error_code(); } +std::string InspectError::error_msg() { + std::string msg; + err_->error_msg(msg); + return msg; +} +std::optional InspectError::required_buffer_size() { + std::size_t buf_size{0}; + if (err_->required_buffer_size(buf_size)) { + return buf_size; + } else { + return std::nullopt; + } +} + +std::int16_t InspectResponse::status() const { return ir_->status(); } + +std::optional InspectResponse::redirect_url() const { + std::string ret; + if (ir_->redirect_url(ret)) { + return ret; + } + return std::nullopt; +} + +std::vector InspectResponse::tags() const { + auto tags = ir_->tags(); + std::vector ret; + ret.resize(tags.size()); + std::transform(tags.begin(), tags.end(), std::back_inserter(ret), + [](auto str) { return std::string(str); }); + return ret; +} + +InspectVerdict InspectResponse::verdict() const { return ir_->verdict(); } +std::optional InspectResponse::unrecognized_verdict_info() const { + std::string ret; + if (ir_->unrecognized_verdict_info(ret)) { + return ret; + } + return std::nullopt; +} + +std::chrono::milliseconds InspectResponse::decision_ms() const { + return std::chrono::milliseconds(ir_->decision_ms()); +} + +bool InspectResponse::is_redirect() const { return ir_->is_redirect(); } +std::optional InspectResponse::into_redirect() { + fastly::sys::http::Response *resp; + if (fastly::sys::security::m_security_inspect_response_into_redirect( + std::move(ir_), resp)) { + return detail::AccessBridgeInternals::from_raw(resp); + } + return std::nullopt; +} + +tl::expected +inspect(fastly::http::Request &request, InspectConfig config) { + fastly::sys::security::InspectResponse *out; + fastly::sys::security::InspectError *err; + auto client_ip = config.client_ip() ? &*config.client_ip() : nullptr; + auto corp = config.corp() ? &*config.corp() : nullptr; + auto workspace = config.workspace() ? &*config.workspace() : nullptr; + auto buffer_size = config.buffer_size() ? &*config.buffer_size() : nullptr; + fastly::sys::security::f_security_lookup( + *detail::AccessBridgeInternals::get(request), client_ip, corp, workspace, + buffer_size, out, err); + if (err == nullptr) { + return detail::AccessBridgeInternals::from_raw(out); + } else { + return tl::unexpected(err); + } +} +} // namespace fastly::security diff --git a/src/lib.rs b/src/lib.rs index 6bf491a..414aa42 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ use http::{ use kv_store::*; use log::*; use secret_store::*; +use security::*; mod backend; mod config_store; @@ -25,6 +26,7 @@ mod http; mod kv_store; mod log; mod secret_store; +mod security; // Unfortunately, due to some limitations with cxx, the ENTIRE bridge basically // has to be under a single ffi module, or cross-referencing ffi types is gonna @@ -817,6 +819,74 @@ mod ffi { fn init(&mut self); } + #[namespace = "fastly::sys::security"] + #[derive(Copy, Clone, Debug)] + #[repr(usize)] + pub enum InspectErrorCode { + DeserializeError, + InvalidConfig, + RequestError, + BufferSizeError, + Unexpected, + } + + #[namespace = "fastly::sys::security"] + #[derive(Copy, Clone, Debug)] + pub enum InspectVerdict { + /// Security indicated that this request is allowed. + Allow, + + /// Security indicated that this request should be blocked. + Block, + + /// Security indicated that this service is not authorized to inspect a request. + Unauthorized, + + /// Security returned an unrecognized verdict. + /// + /// This variant exists to allow for the possibility of future + /// additions, but should normally not be seen. + Other, + } + + #[namespace = "fastly::sys::security"] + extern "Rust" { + type InspectResponse; + fn status(&self) -> i16; + fn is_redirect(&self) -> bool; + fn decision_ms(&self) -> u32; + fn redirect_url(&self, out: Pin<&mut CxxString>) -> bool; + fn tags(&self) -> Vec; + fn verdict(&self) -> InspectVerdict; + fn unrecognized_verdict_info(&self, out: Pin<&mut CxxString>) -> bool; + } + + #[namespace = "fastly::sys::security"] + extern "Rust" { + fn m_security_inspect_response_into_redirect( + response: Box, + mut out: Pin<&mut *mut Response>, + ) -> bool; + unsafe fn f_security_lookup( + request: &Request, + client_ip: *const CxxString, + corp: *const CxxString, + workspace: *const CxxString, + buffer_size: *const usize, + mut out: Pin<&mut *mut InspectResponse>, + mut err: Pin<&mut *mut InspectError>, + ); + } + + #[namespace = "fastly::sys::security"] + extern "Rust" { + type InspectError; + fn error_msg(&self, mut out: Pin<&mut CxxString>); + fn error_code(&self) -> InspectErrorCode; + fn required_buffer_size(&self, out: Pin<&mut usize>) -> bool; + fn f_security_inspect_error_force_symbols(x: Box) -> Box; + } + #[namespace = "fastly::sys::kv_store"] #[derive(Copy, Clone, Debug)] #[repr(usize)] diff --git a/src/security.rs b/src/security.rs new file mode 100644 index 0000000..7ce7d0a --- /dev/null +++ b/src/security.rs @@ -0,0 +1,161 @@ +use std::{io::Write as _, net::IpAddr, pin::Pin, str::FromStr}; + +use cxx::CxxString; + +use crate::{ + ffi::{InspectErrorCode, InspectVerdict}, + http::request::Request, + http::response::Response, +}; + +pub struct InspectResponse(pub(crate) fastly::security::InspectResponse); + +pub struct InspectError(pub(crate) fastly::security::InspectError); + +impl InspectError { + pub fn error_msg(&self, mut out: Pin<&mut CxxString>) { + write!(out, "{}", self.0).expect("This should never fail."); + } + + pub fn error_code(&self) -> InspectErrorCode { + match self.0 { + fastly::security::InspectError::DeserializeError(_) => { + InspectErrorCode::DeserializeError + } + fastly::security::InspectError::InvalidConfig => InspectErrorCode::InvalidConfig, + fastly::security::InspectError::RequestError(_) => InspectErrorCode::RequestError, + fastly::security::InspectError::BufferSizeError(_) => InspectErrorCode::BufferSizeError, + _ => InspectErrorCode::Unexpected, + } + } + + pub fn required_buffer_size(&self, mut out: Pin<&mut usize>) -> bool { + match self.0 { + fastly::security::InspectError::BufferSizeError(n) => { + out.set(n); + true + } + _ => false, + } + } +} + +#[macro_export] +macro_rules! try_ie { + ( $err:ident, $x:expr ) => { + match $x { + std::result::Result::Ok(val) => { + $err.set(std::ptr::null_mut()); + val + } + std::result::Result::Err(e) => { + $err.set(Box::into_raw(Box::new(InspectError(e)))); + return Default::default(); + } + } + }; +} + +pub fn f_security_lookup( + request: &Request, + client_ip: *const CxxString, + corp: *const CxxString, + workspace: *const CxxString, + buffer_size: *const usize, + mut out: Pin<&mut *mut InspectResponse>, + mut err: Pin<&mut *mut InspectError>, +) { + let mut icfg = fastly::security::InspectConfig::from_request(&request.0); + unsafe { + if let Some(client_ip) = client_ip.as_ref() { + let ip = IpAddr::from_str(try_ie!( + err, + client_ip + .to_str() + .map_err(|_| fastly::security::InspectError::InvalidConfig) + )); + let ip = try_ie!( + err, + ip.map_err(|_| fastly::security::InspectError::InvalidConfig) + ); + icfg = icfg.client_ip(ip); + } + if let Some(corp) = corp.as_ref() { + icfg = icfg.corp(corp.to_string()); + } + if let Some(workspace) = workspace.as_ref() { + icfg = icfg.workspace(workspace.to_string()); + } + if let Some(buffer_size) = buffer_size.as_ref() { + icfg = icfg.buffer_size(*buffer_size); + } + } + out.set(try_ie!( + err, + fastly::security::inspect(icfg) + .map(InspectResponse) + .map(Box::new) + .map(Box::into_raw) + )) +} + +impl InspectResponse { + pub fn status(&self) -> i16 { + self.0.status() + } + + pub fn is_redirect(&self) -> bool { + self.0.is_redirect() + } + + pub fn decision_ms(&self) -> u32 { + self.0.decision_ms().as_millis().min(u32::MAX as u128) as u32 + } + + pub fn redirect_url(&self, out: Pin<&mut CxxString>) -> bool { + if let Some(url) = self.0.redirect_url() { + out.push_str(url); + true + } else { + false + } + } + + pub fn tags(&self) -> Vec { + self.0.tags().into_iter().map(|x| x.into()).collect() + } + + pub fn verdict(&self) -> InspectVerdict { + match self.0.verdict() { + fastly::security::InspectVerdict::Allow => InspectVerdict::Allow, + fastly::security::InspectVerdict::Block => InspectVerdict::Block, + fastly::security::InspectVerdict::Unauthorized => InspectVerdict::Unauthorized, + fastly::security::InspectVerdict::Other(_) => InspectVerdict::Other, + } + } + + pub fn unrecognized_verdict_info(&self, out: Pin<&mut CxxString>) -> bool { + if let fastly::security::InspectVerdict::Other(s) = self.0.verdict() { + out.push_str(s); + true + } else { + false + } + } +} + +pub fn m_security_inspect_response_into_redirect( + response: Box, + mut out: Pin<&mut *mut Response>, +) -> bool { + if let Some(resp) = response.0.into_redirect() { + out.set(Box::into_raw(Box::new(Response(resp)))); + true + } else { + false + } +} + +pub fn f_security_inspect_error_force_symbols(x: Box) -> Box { + x +} diff --git a/test/security.cpp b/test/security.cpp new file mode 100644 index 0000000..9543e63 --- /dev/null +++ b/test/security.cpp @@ -0,0 +1,41 @@ +#include +#include + +using namespace fastly::security; + +TEST_CASE("Mock NGWAF returns allow", "[security]") { + auto request = fastly::http::Request::get("https://example.com"); + auto config = InspectConfig() + .with_corp("test") + .with_workspace("test") + .with_buffer_size(256) + .with_client_ip("10.10.10.10"); + auto res = inspect(request, config); + REQUIRE(res.has_value()); + REQUIRE(res->verdict() == InspectVerdict::Allow); +} + +// This is currently the case in the implementation, but not reflected in the +// API because it is planned to change +TEST_CASE("NGWAF requires corp and workspace", "[security]") { + auto request = fastly::http::Request::get("https://example.com"); + SECTION("Missing corp") { + auto res = inspect(request, InspectConfig().with_workspace("test")); + REQUIRE(!res.has_value()); + REQUIRE(res.error().error_code() == InspectErrorCode::InvalidConfig); + } + SECTION("Missing workspace") { + auto res = inspect(request, InspectConfig().with_corp("test")); + REQUIRE(!res.has_value()); + REQUIRE(res.error().error_code() == InspectErrorCode::InvalidConfig); + } + SECTION("Missing both") { + auto res = inspect(request, InspectConfig()); + REQUIRE(!res.has_value()); + REQUIRE(res.error().error_code() == InspectErrorCode::InvalidConfig); + } +} + +// Required due to https://github.com/WebAssembly/wasi-libc/issues/485 +#include +int main(int argc, char *argv[]) { return Catch::Session().run(argc, argv); } \ No newline at end of file