Skip to content

Commit 7e8b441

Browse files
committed
feat(inspect): add support for NGWAF inspect api
1 parent e273df4 commit 7e8b441

File tree

13 files changed

+326
-7
lines changed

13 files changed

+326
-7
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
hide_title: false
3+
hide_table_of_contents: false
4+
pagination_next: null
5+
pagination_prev: null
6+
---
7+
8+
# inspect
9+
10+
The **`inspect()`** inspect a request using the [Fastly Next-Gen WAF](https://docs.fastly.com/en/ngwaf/).
11+
12+
## Syntax
13+
14+
```js
15+
inspect(request, config);
16+
```
17+
18+
### Parameters
19+
20+
- `request` _: Request_
21+
- The Request to get a WAF determination for.
22+
- `config` _: object_
23+
- `corp` _: string_
24+
- Set a corp name for the configuration.
25+
- `workspace` _: string_
26+
- Set a workspace name for the configuration.
27+
- `overrideClientIp` _: string_
28+
- Specify an explicit client IP address to inspect.
29+
- By default, `inspect` will use the IP address that made the request to the
30+
running Compute service, but you may want to use a different IP when
31+
service chaining or if requests are proxied from outside of Fastly’s
32+
network.
33+
34+
### Return value
35+
36+
Returns an `Object` with the `inspect` response, with the following fields:
37+
38+
- `response` _: number_
39+
- Security status code.
40+
41+
- `redirect_url` _: string | null_
42+
- A redirect URL returned from Security.
43+
44+
- `tags` _: string[]_
45+
- Tags returned by Security.
46+
47+
- `verdict` _: string_
48+
- The outcome of inspecting a request with Security. It can be one of the following:
49+
- `"allow"`
50+
- Security indicated that this request is allowed.
51+
- `"block"`
52+
- Security indicated that this request should be blocked.
53+
- `"unauthorized"`
54+
- Security indicated that this service is not authorized to inspect a request.
55+
- Other verdicts may be returned but not currently documented.
56+
57+
- `decision_ms` _: number_
58+
- How long Security spent determining its verdict, in milliseconds.
59+
60+
## Examples
61+
62+
<!-- TODO(@zkat): Write out an example -->

integration-tests/js-compute/fixtures/app/__input_bundled.js.map

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/js-compute/fixtures/app/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import './response-json.js';
4646
import './response-redirect.js';
4747
import './response.js';
4848
import './secret-store.js';
49+
import './security.js';
4950
import './server.js';
5051
import './shielding.js';
5152
import './tee.js';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* eslint-env serviceworker */
2+
/* global fastly */
3+
import { assert, assertThrows, strictEqual } from './assertions.js';
4+
import { inspect } from 'fastly:security';
5+
import { routes } from './routes.js';
6+
7+
routes.set('/fastly/security/inspect/invalid-config', () => {
8+
const req = new Request('https://example.com');
9+
assertThrows(() => {
10+
inspect(req);
11+
});
12+
assertThrows(() => {
13+
inspect(req, {
14+
corp: 'test',
15+
});
16+
});
17+
assertThrows(() => {
18+
inspect(req, {
19+
workspace: 'test',
20+
});
21+
});
22+
});
23+
24+
routes.set('/fastly/security/inspect/basic', () => {
25+
const req = new Request('https://example.com');
26+
const config = {
27+
corp: 'test',
28+
workspace: 'test',
29+
overrideClientIp: '10.10.10.10',
30+
};
31+
const result = inspect(req, config);
32+
assert(strictEqual(result.verdict, 'allow'));
33+
assert(strictEqual(result.response, 123)); // TODO(@zkat): What should this be?
34+
assert(Array.isArray(result.tags));
35+
assert(strictEqual(typeof result.decision_ms, 'number'));
36+
assert(result.decision_ms >= 0);
37+
});

integration-tests/js-compute/fixtures/app/tests.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3139,5 +3139,11 @@
31393139
},
31403140
"GET /shielding/invalid-shield": {
31413141
"environments": ["compute"]
3142+
},
3143+
"GET /fastly/security/inspect/invalid-config": {
3144+
"environments": ["compute"]
3145+
},
3146+
"GET /fastly/security/inspect/basic": {
3147+
"environments": ["compute"]
31423148
}
31433149
}

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

runtime/fastly/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ cmake_minimum_required(VERSION 3.27)
33
#FIXME(1243)
44
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/../rust-toolchain.toml" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/../StarlingMonkey")
55

6+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
7+
68
include("../StarlingMonkey/cmake/add_as_subproject.cmake")
79

810
add_builtin(

runtime/fastly/builtins/fastly.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,121 @@ bool Fastly::getGeolocationForIpAddress(JSContext *cx, unsigned argc, JS::Value
169169
return JS_ParseJSON(cx, geo_info_str, args.rval());
170170
}
171171

172+
bool Fastly::inspect(JSContext *cx, unsigned argc, JS::Value *vp) {
173+
JS::CallArgs args = CallArgsFromVp(argc, vp);
174+
REQUEST_HANDLER_ONLY("fastly.inspect");
175+
if (!args.requireAtLeast(cx, "fastly.inspect", 1)) {
176+
return false;
177+
}
178+
179+
auto request_value = args.get(0);
180+
if (!Request::is_instance(request_value)) {
181+
JS_ReportErrorUTF8(cx, "inspect: request parameter must be an instance of Request");
182+
return false;
183+
}
184+
auto request = &request_value.toObject();
185+
auto req{Request::request_handle(request)};
186+
auto bod{RequestOrResponse::body_handle(request)};
187+
188+
auto options_value = args.get(1);
189+
JS::RootedObject options_obj(cx, options_value.isObject() ? &options_value.toObject() : nullptr);
190+
191+
host_api::InspectOptions inspect_options(req.handle, bod.handle);
192+
193+
if (options_obj != nullptr) {
194+
host_api::HostString corp_str;
195+
JS::RootedValue corp_val(cx);
196+
if (!JS_GetProperty(cx, options_obj, "corp", &corp_val)) {
197+
return false;
198+
}
199+
if (!corp_val.isNullOrUndefined()) {
200+
if (!corp_val.isString()) {
201+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "corp", "be a string");
202+
return false;
203+
}
204+
corp_str = core::encode(cx, corp_val);
205+
if (!corp_str) {
206+
return false;
207+
}
208+
std::optional<std::string_view> corp = corp_str;
209+
if (corp) {
210+
inspect_options.corp_len = corp->length();
211+
inspect_options.corp = std::move(corp->data());
212+
}
213+
}
214+
215+
host_api::HostString workspace_str;
216+
JS::RootedValue workspace_val(cx);
217+
if (!JS_GetProperty(cx, options_obj, "workspace", &workspace_val)) {
218+
return false;
219+
}
220+
if (!workspace_val.isNullOrUndefined()) {
221+
if (!workspace_val.isString()) {
222+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "workspace", "be a string");
223+
return false;
224+
}
225+
workspace_str = core::encode(cx, workspace_val);
226+
if (!workspace_str) {
227+
return false;
228+
}
229+
std::optional<std::string_view> workspace = workspace_str;
230+
if (workspace) {
231+
inspect_options.workspace_len = workspace->length();
232+
inspect_options.workspace = std::move(workspace->data());
233+
}
234+
}
235+
236+
host_api::HostString override_client_ip_str;
237+
JS::RootedValue override_client_ip_val(cx);
238+
std::vector<char> octets;
239+
if (!JS_GetProperty(cx, options_obj, "overrideClientIp", &override_client_ip_val)) {
240+
return false;
241+
}
242+
if (!override_client_ip_val.isNullOrUndefined()) {
243+
if (!override_client_ip_val.isString()) {
244+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "overrideClientIp",
245+
"be a string");
246+
return false;
247+
}
248+
override_client_ip_str = core::encode(cx, override_client_ip_val);
249+
if (!override_client_ip_str) {
250+
return false;
251+
}
252+
253+
// TODO: Remove all of this and rely on the host for validation as the hostcall only takes one
254+
// user-supplied parameter
255+
int format = AF_INET;
256+
size_t octets_len = 4;
257+
if (std::find(override_client_ip_str.begin(), override_client_ip_str.end(), ':') !=
258+
override_client_ip_str.end()) {
259+
format = AF_INET6;
260+
octets_len = 16;
261+
}
262+
263+
octets.reserve(sizeof(struct in6_addr));
264+
if (inet_pton(format, override_client_ip_str.begin(), octets.data()) != 1) {
265+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "overrideClientIp",
266+
"be a valid IP address");
267+
return false;
268+
}
269+
inspect_options.override_client_ip_len = octets_len;
270+
inspect_options.override_client_ip_ptr = octets.data();
271+
}
272+
}
273+
274+
host_api::Request host_request{req, bod};
275+
auto res = host_request.inspect(&inspect_options);
276+
if (auto *err = res.to_err()) {
277+
HANDLE_ERROR(cx, *err);
278+
return false;
279+
}
280+
281+
auto ret{std::move(res.unwrap())};
282+
283+
JS::RootedString js_str(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(ret.ptr.release(), ret.len)));
284+
return JS_ParseJSON(cx, js_str, args.rval());
285+
}
286+
172287
// TODO(performance): consider allowing logger creation during initialization, but then throw
173288
// when trying to log.
174289
// https://github.com/fastly/js-compute-runtime/issues/225
@@ -617,6 +732,7 @@ bool install(api::Engine *engine) {
617732
JS_FN("enableDebugLogging", Fastly::enableDebugLogging, 1, JSPROP_ENUMERATE),
618733
JS_FN("debugLog", debugLog, 1, JSPROP_ENUMERATE),
619734
JS_FN("getGeolocationForIpAddress", Fastly::getGeolocationForIpAddress, 1, JSPROP_ENUMERATE),
735+
JS_FN("inspect", Fastly::inspect, 1, JSPROP_ENUMERATE),
620736
JS_FN("getLogger", Fastly::getLogger, 1, JSPROP_ENUMERATE),
621737
JS_FN("includeBytes", Fastly::includeBytes, 1, JSPROP_ENUMERATE),
622738
JS_FN("createFanoutHandoff", Fastly::createFanoutHandoff, 2, JSPROP_ENUMERATE),
@@ -758,6 +874,21 @@ bool install(api::Engine *engine) {
758874
if (!engine->define_builtin_module("fastly:fanout", fanout_val)) {
759875
return false;
760876
}
877+
878+
// fastly:security
879+
RootedValue inspect_val(engine->cx());
880+
if (!JS_GetProperty(engine->cx(), fastly, "inspect", &inspect_val)) {
881+
return false;
882+
}
883+
RootedObject security_builtin(engine->cx(), JS_NewObject(engine->cx(), nullptr));
884+
RootedValue security_builtin_val(engine->cx(), JS::ObjectValue(*security_builtin));
885+
if (!JS_SetProperty(engine->cx(), security_builtin, "inspect", inspect_val)) {
886+
return false;
887+
}
888+
if (!engine->define_builtin_module("fastly:security", security_builtin_val)) {
889+
return false;
890+
}
891+
761892
// fastly:websocket
762893
RootedObject websocket(engine->cx(), JS_NewObject(engine->cx(), nullptr));
763894
RootedValue websocket_val(engine->cx(), JS::ObjectValue(*websocket));

runtime/fastly/builtins/fastly.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class Fastly : public builtins::BuiltinNoConstructor<Fastly> {
6060
static bool defaultBackend_set(JSContext *cx, unsigned argc, JS::Value *vp);
6161
static bool allowDynamicBackends_get(JSContext *cx, unsigned argc, JS::Value *vp);
6262
static bool allowDynamicBackends_set(JSContext *cx, unsigned argc, JS::Value *vp);
63+
static bool inspect(JSContext *cx, unsigned argc, JS::Value *vp);
6364
};
6465

6566
JS::Result<std::tuple<JS::UniqueChars, size_t>> convertBodyInit(JSContext *cx,

runtime/fastly/host-api/fastly.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ typedef struct fastly_host_http_response {
3939
uint32_t f1;
4040
} fastly_host_http_response;
4141

42+
typedef struct fastly_host_http_inspect_options {
43+
const char *corp;
44+
uint32_t corp_len;
45+
const char *workspace;
46+
uint32_t workspace_len;
47+
const char *override_client_ip_ptr;
48+
uint32_t override_client_ip_len;
49+
} fastly_host_http_inspect_options;
50+
4251
typedef fastly_host_http_response fastly_world_tuple2_handle_handle;
4352

4453
#define WASM_IMPORT(module, name) __attribute__((import_module(module), import_name(name)))
@@ -265,6 +274,13 @@ typedef enum BodyWriteEnd {
265274
#define CACHE_OVERRIDE_STALE_WHILE_REVALIDATE (1u << 2)
266275
#define CACHE_OVERRIDE_PCI (1u << 3)
267276

277+
typedef uint32_t req_inspect_config_options_mask;
278+
279+
#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_RESERVED (1 << 0);
280+
#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_CORP (1 << 1);
281+
#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_WORKSPACE (1 << 2);
282+
#define FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_OVERRIDE_CLIENT_IP (1 << 3);
283+
268284
WASM_IMPORT("fastly_abi", "init")
269285
int init(uint64_t abi_version);
270286

@@ -629,6 +645,12 @@ int req_pending_req_wait_v2(uint32_t req_handle,
629645
fastly_host_http_send_error_detail *send_error_detail,
630646
uint32_t *resp_handle_out, uint32_t *resp_body_handle_out);
631647

648+
WASM_IMPORT("fastly_http_req", "inspect")
649+
int req_inspect(uint32_t req_handle, uint32_t body_handle,
650+
req_inspect_config_options_mask config_options_mask,
651+
fastly_host_http_inspect_options *config, uint8_t *inspect_res_buf,
652+
uint32_t inspect_res_buf_len, size_t *nwritten_out);
653+
632654
// Module fastly_http_resp
633655
WASM_IMPORT("fastly_http_resp", "new")
634656
int resp_new(uint32_t *resp_handle_out);

0 commit comments

Comments
 (0)