Skip to content

Commit c7ef0eb

Browse files
committed
feat(inspect): add support for NGWAF inspect api
1 parent 3d60085 commit c7ef0eb

File tree

14 files changed

+370
-7
lines changed

14 files changed

+370
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ dist/
55
/integration-tests/**/fixtures/**/*.tar.gz
66
/integration-tests/**/fixtures/**/fastly.toml
77
/integration-tests/**/fixtures/**/*.wasm
8+
/integration-tests/**/fixtures/**/*.js.map
89

910
/runtime/fastly/build-*/
1011

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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()`** function inspects 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+
- This parameter is currently required.
26+
- `workspace` _: string_
27+
- Set a workspace name for the configuration.
28+
- This parameter is currently required.
29+
- `overrideClientIp` _: string_
30+
- Specify an explicit client IP address to inspect.
31+
- By default, `inspect` will use the IP address that made the request to the
32+
running Compute service, but you may want to use a different IP when
33+
service chaining or if requests are proxied from outside of Fastly’s
34+
network.
35+
36+
### Return value
37+
38+
Returns an `Object` with the `inspect` response, with the following fields:
39+
40+
- `waf_response` _: number_
41+
- Security status code.
42+
43+
- `redirect_url` _: string | null_
44+
- A redirect URL returned from Security.
45+
46+
- `tags` _: string[]_
47+
- Tags returned by Security.
48+
49+
- `verdict` _: string_
50+
- The outcome of inspecting a request with Security. It can be one of the following:
51+
- `"allow"`
52+
- Security indicated that this request is allowed.
53+
- `"block"`
54+
- Security indicated that this request should be blocked.
55+
- `"unauthorized"`
56+
- Security indicated that this service is not authorized to inspect a request.
57+
- Other verdicts may be returned but not currently documented.
58+
59+
- `decision_ms` _: number_
60+
- How long Security spent determining its verdict, in milliseconds.
61+
62+
## Examples
63+
64+
```js
65+
/// <reference types="@fastly/js-compute" />
66+
67+
import { inspect } from "fastly:security";
68+
69+
async function app(event) {
70+
const res = inspect(event.request, {
71+
corp: "mycorp",
72+
workspace: "myws"
73+
});
74+
switch (res.verdict) {
75+
case "allow":
76+
return await fetch(event.request);
77+
case "block":
78+
return new Response("Request Blocked", { status: 400 });
79+
case "unauthorized":
80+
return new Response("Unauthorized", { status: 401 });
81+
default:
82+
return new Response("idk", { status: 500 });
83+
}
84+
}
85+
86+
addEventListener("fetch", (event) => event.respondWith(app(event)));
87+
```

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

Lines changed: 0 additions & 7 deletions
This file was deleted.

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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/* eslint-env serviceworker */
2+
import { assert, assertThrows, strictEqual } from './assertions.js';
3+
import { inspect } from 'fastly:security';
4+
import { routes } from './routes.js';
5+
6+
routes.set('/fastly/security/inspect/invalid-config', () => {
7+
const req = new Request('https://example.com');
8+
assertThrows(() => {
9+
inspect(req);
10+
});
11+
assertThrows(() => {
12+
inspect(req, {
13+
corp: 'test',
14+
});
15+
});
16+
assertThrows(() => {
17+
inspect(req, {
18+
workspace: 'test',
19+
});
20+
});
21+
});
22+
23+
routes.set('/fastly/security/inspect/basic', () => {
24+
const req = new Request('https://example.com');
25+
const config = {
26+
corp: 'test',
27+
workspace: 'test',
28+
overrideClientIp: '10.10.10.10',
29+
};
30+
const result = inspect(req, config);
31+
strictEqual(result.verdict, 'allow');
32+
strictEqual(result.waf_response, 200);
33+
strictEqual(Array.isArray(result.tags), true);
34+
strictEqual(typeof result.decision_ms, 'number');
35+
assert(result.decision_ms >= 0, true);
36+
});

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": ["viceroy"]
3145+
},
3146+
"GET /fastly/security/inspect/basic": {
3147+
"environments": ["viceroy"]
31423148
}
31433149
}

runtime/fastly/builtins/fastly.cpp

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,115 @@ 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+
JS::RootedObject request(cx, &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+
host_api::HostString corp_str;
194+
JS::RootedValue corp_val(cx);
195+
196+
host_api::HostString workspace_str;
197+
JS::RootedValue workspace_val(cx);
198+
199+
host_api::HostString override_client_ip_str;
200+
JS::RootedValue override_client_ip_val(cx);
201+
alignas(struct in6_addr) uint8_t octets[sizeof(struct in6_addr)];
202+
203+
if (options_obj != nullptr) {
204+
if (JS_GetProperty(cx, options_obj, "corp", &corp_val)) {
205+
if (!corp_val.isNullOrUndefined()) {
206+
if (!corp_val.isString()) {
207+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "corp", "be a string");
208+
return false;
209+
}
210+
corp_str = core::encode(cx, corp_val);
211+
if (!corp_str) {
212+
return false;
213+
}
214+
inspect_options.corp_len = corp_str.size();
215+
inspect_options.corp = std::move(corp_str.begin());
216+
}
217+
}
218+
219+
if (JS_GetProperty(cx, options_obj, "workspace", &workspace_val)) {
220+
if (!workspace_val.isNullOrUndefined()) {
221+
if (!workspace_val.isString()) {
222+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "workspace",
223+
"be a string");
224+
return false;
225+
}
226+
workspace_str = core::encode(cx, workspace_val);
227+
if (!workspace_str) {
228+
return false;
229+
}
230+
inspect_options.workspace_len = workspace_str.size();
231+
inspect_options.workspace = std::move(workspace_str.begin());
232+
}
233+
}
234+
235+
if (JS_GetProperty(cx, options_obj, "overrideClientIp", &override_client_ip_val)) {
236+
if (!override_client_ip_val.isNullOrUndefined()) {
237+
if (!override_client_ip_val.isString()) {
238+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "overrideClientIp",
239+
"be a string");
240+
return false;
241+
}
242+
override_client_ip_str = core::encode(cx, override_client_ip_val);
243+
if (!override_client_ip_str) {
244+
return false;
245+
}
246+
247+
// TODO: Remove all of this and rely on the host for validation as the hostcall only takes
248+
// one user-supplied parameter
249+
int format = AF_INET;
250+
size_t octets_len = 4;
251+
if (std::find(override_client_ip_str.begin(), override_client_ip_str.end(), ':') !=
252+
override_client_ip_str.end()) {
253+
format = AF_INET6;
254+
octets_len = 16;
255+
}
256+
257+
if (inet_pton(format, override_client_ip_str.begin(), &octets) != 1) {
258+
api::throw_error(cx, api::Errors::TypeError, "fastly.inspect", "overrideClientIp",
259+
"be a valid IP address");
260+
return false;
261+
}
262+
inspect_options.override_client_ip_len = octets_len;
263+
inspect_options.override_client_ip_ptr = reinterpret_cast<const char *>(&octets);
264+
}
265+
}
266+
}
267+
268+
host_api::Request host_request{req, bod};
269+
auto res = host_request.inspect(&inspect_options);
270+
if (auto *err = res.to_err()) {
271+
HANDLE_ERROR(cx, *err);
272+
return false;
273+
}
274+
275+
auto ret{std::move(res.unwrap())};
276+
277+
JS::RootedString js_str(cx, JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(ret.ptr.release(), ret.len)));
278+
return JS_ParseJSON(cx, js_str, args.rval());
279+
}
280+
172281
// TODO(performance): consider allowing logger creation during initialization, but then throw
173282
// when trying to log.
174283
// https://github.com/fastly/js-compute-runtime/issues/225
@@ -617,6 +726,7 @@ bool install(api::Engine *engine) {
617726
JS_FN("enableDebugLogging", Fastly::enableDebugLogging, 1, JSPROP_ENUMERATE),
618727
JS_FN("debugLog", debugLog, 1, JSPROP_ENUMERATE),
619728
JS_FN("getGeolocationForIpAddress", Fastly::getGeolocationForIpAddress, 1, JSPROP_ENUMERATE),
729+
JS_FN("inspect", Fastly::inspect, 1, JSPROP_ENUMERATE),
620730
JS_FN("getLogger", Fastly::getLogger, 1, JSPROP_ENUMERATE),
621731
JS_FN("includeBytes", Fastly::includeBytes, 1, JSPROP_ENUMERATE),
622732
JS_FN("createFanoutHandoff", Fastly::createFanoutHandoff, 2, JSPROP_ENUMERATE),
@@ -758,6 +868,21 @@ bool install(api::Engine *engine) {
758868
if (!engine->define_builtin_module("fastly:fanout", fanout_val)) {
759869
return false;
760870
}
871+
872+
// fastly:security
873+
RootedValue inspect_val(engine->cx());
874+
if (!JS_GetProperty(engine->cx(), fastly, "inspect", &inspect_val)) {
875+
return false;
876+
}
877+
RootedObject security_builtin(engine->cx(), JS_NewObject(engine->cx(), nullptr));
878+
RootedValue security_builtin_val(engine->cx(), JS::ObjectValue(*security_builtin));
879+
if (!JS_SetProperty(engine->cx(), security_builtin, "inspect", inspect_val)) {
880+
return false;
881+
}
882+
if (!engine->define_builtin_module("fastly:security", security_builtin_val)) {
883+
return false;
884+
}
885+
761886
// fastly:websocket
762887
RootedObject websocket(engine->cx(), JS_NewObject(engine->cx(), nullptr));
763888
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);

runtime/fastly/host-api/host_api.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2265,6 +2265,43 @@ Result<HostString> HttpReq::get_suggested_cache_key() const {
22652265
return Result<HostString>::ok(make_host_string(str));
22662266
}
22672267

2268+
Result<HostString> Request::inspect(const InspectOptions *config) {
2269+
TRACE_CALL()
2270+
Result<HostString> res;
2271+
uint32_t inspect_opts_mask{0};
2272+
fastly::fastly_host_http_inspect_options opts;
2273+
2274+
if (config->corp != nullptr) {
2275+
inspect_opts_mask |= FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_CORP;
2276+
opts.corp = config->corp;
2277+
opts.corp_len = config->corp_len;
2278+
}
2279+
2280+
if (config->workspace != nullptr) {
2281+
inspect_opts_mask |= FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_WORKSPACE;
2282+
opts.workspace = config->workspace;
2283+
opts.workspace_len = config->workspace_len;
2284+
}
2285+
2286+
if (config->override_client_ip_ptr != nullptr) {
2287+
inspect_opts_mask |= FASTLY_HOST_HTTP_REQ_INSPECT_CONFIG_OPTIONS_MASK_OVERRIDE_CLIENT_IP;
2288+
opts.override_client_ip_ptr = config->override_client_ip_ptr;
2289+
opts.override_client_ip_len = config->override_client_ip_len;
2290+
}
2291+
2292+
fastly::fastly_host_error err;
2293+
fastly::fastly_world_string ret;
2294+
ret.ptr = static_cast<uint8_t *>(cabi_malloc(HOSTCALL_BUFFER_LEN, 4));
2295+
if (!convert_result(fastly::req_inspect(this->req.handle, this->body.handle, inspect_opts_mask,
2296+
&opts, ret.ptr, HOSTCALL_BUFFER_LEN, &ret.len),
2297+
&err)) {
2298+
res.emplace_err(err);
2299+
} else {
2300+
res.emplace(make_host_string(ret));
2301+
}
2302+
return res;
2303+
}
2304+
22682305
// HttpCacheEntry method implementations
22692306
Result<HttpCacheEntry> HttpCacheEntry::lookup(const HttpReq &req, std::span<uint8_t> override_key) {
22702307
TRACE_CALL()

0 commit comments

Comments
 (0)