2026-03-10 23:38:22 +00:00
|
|
|
// Copyright Sunbeam Studios 2026
|
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
|
|
|
|
//! Scanner detection tests.
|
|
|
|
|
//!
|
|
|
|
|
//! The detector uses ensemble inference (decision tree + MLP) with compiled-in
|
|
|
|
|
//! weights. These tests exercise the allowlist and ensemble classification paths.
|
|
|
|
|
|
2026-03-10 23:38:19 +00:00
|
|
|
use sunbeam_proxy::config::RouteConfig;
|
|
|
|
|
use sunbeam_proxy::scanner::detector::ScannerDetector;
|
2026-03-10 23:38:22 +00:00
|
|
|
use sunbeam_proxy::scanner::model::ScannerAction;
|
2026-03-10 23:38:19 +00:00
|
|
|
|
|
|
|
|
fn test_routes() -> Vec<RouteConfig> {
|
|
|
|
|
vec![
|
|
|
|
|
RouteConfig {
|
|
|
|
|
host_prefix: "app".into(),
|
|
|
|
|
backend: "http://127.0.0.1:8080".into(),
|
|
|
|
|
websocket: false,
|
|
|
|
|
disable_secure_redirection: false,
|
|
|
|
|
paths: vec![],
|
feat(static_files): add static file serving, SPA fallback, rewrites, body rewriting, and auth subrequests
Add static file serving with try_files chain ($uri, $uri.html,
$uri/index.html, fallback), regex-based URL rewrites compiled at
startup, response body find/replace for text/html and JS content,
auth subrequests with header capture for path routes, and custom
response headers per route. Extends RouteConfig with static_root,
fallback, rewrites, body_rewrites, and response_headers fields.
Signed-off-by: Sienna Meridian Satterwhite <sienna@sunbeam.pt>
2026-03-10 23:38:20 +00:00
|
|
|
static_root: None,
|
|
|
|
|
fallback: None,
|
|
|
|
|
rewrites: vec![],
|
|
|
|
|
body_rewrites: vec![],
|
|
|
|
|
response_headers: vec![],
|
2026-03-10 23:38:20 +00:00
|
|
|
cache: None,
|
2026-03-10 23:38:19 +00:00
|
|
|
},
|
|
|
|
|
RouteConfig {
|
|
|
|
|
host_prefix: "api".into(),
|
|
|
|
|
backend: "http://127.0.0.1:8081".into(),
|
|
|
|
|
websocket: false,
|
|
|
|
|
disable_secure_redirection: false,
|
|
|
|
|
paths: vec![],
|
feat(static_files): add static file serving, SPA fallback, rewrites, body rewriting, and auth subrequests
Add static file serving with try_files chain ($uri, $uri.html,
$uri/index.html, fallback), regex-based URL rewrites compiled at
startup, response body find/replace for text/html and JS content,
auth subrequests with header capture for path routes, and custom
response headers per route. Extends RouteConfig with static_root,
fallback, rewrites, body_rewrites, and response_headers fields.
Signed-off-by: Sienna Meridian Satterwhite <sienna@sunbeam.pt>
2026-03-10 23:38:20 +00:00
|
|
|
static_root: None,
|
|
|
|
|
fallback: None,
|
|
|
|
|
rewrites: vec![],
|
|
|
|
|
body_rewrites: vec![],
|
|
|
|
|
response_headers: vec![],
|
2026-03-10 23:38:20 +00:00
|
|
|
cache: None,
|
2026-03-10 23:38:19 +00:00
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn make_detector() -> ScannerDetector {
|
2026-03-10 23:38:22 +00:00
|
|
|
ScannerDetector::new(&test_routes())
|
2026-03-10 23:38:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn normal_browser_with_cookies_allowed() {
|
|
|
|
|
let d = make_detector();
|
|
|
|
|
let v = d.check(
|
|
|
|
|
"GET", "/blog/hello-world", "app",
|
|
|
|
|
true, true, true,
|
|
|
|
|
"text/html,application/xhtml+xml",
|
|
|
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120",
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(v.action, ScannerAction::Allow);
|
|
|
|
|
assert_eq!(v.reason, "allowlist:host+cookies");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn api_client_with_auth_allowed() {
|
|
|
|
|
let d = make_detector();
|
|
|
|
|
let v = d.check(
|
|
|
|
|
"POST", "/api/v1/users", "api",
|
|
|
|
|
true, false, true,
|
|
|
|
|
"application/json",
|
|
|
|
|
"MyApp/2.0",
|
|
|
|
|
256,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(v.action, ScannerAction::Allow);
|
|
|
|
|
assert_eq!(v.reason, "allowlist:host+cookies");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn env_probe_from_unknown_host_blocked() {
|
|
|
|
|
let d = make_detector();
|
|
|
|
|
let v = d.check(
|
|
|
|
|
"GET", "/.env", "unknown",
|
|
|
|
|
false, false, false,
|
|
|
|
|
"*/*", "curl/7.0", 0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(v.action, ScannerAction::Block);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wordpress_scan_blocked() {
|
|
|
|
|
let d = make_detector();
|
|
|
|
|
let v = d.check(
|
|
|
|
|
"GET", "/wp-admin/install.php", "unknown",
|
|
|
|
|
false, false, false,
|
|
|
|
|
"*/*", "", 0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(v.action, ScannerAction::Block);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn path_traversal_blocked() {
|
|
|
|
|
let d = make_detector();
|
|
|
|
|
let v = d.check(
|
|
|
|
|
"GET", "/etc/../../../passwd", "unknown",
|
|
|
|
|
false, false, false,
|
|
|
|
|
"*/*", "python-requests/2.28", 0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(v.action, ScannerAction::Block);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn legitimate_php_path_allowed() {
|
|
|
|
|
let d = make_detector();
|
|
|
|
|
let v = d.check(
|
|
|
|
|
"GET", "/blog/php-is-dead", "app",
|
|
|
|
|
true, true, true,
|
|
|
|
|
"text/html", "Mozilla/5.0 Chrome/120", 0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(v.action, ScannerAction::Allow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn browser_on_known_host_without_cookies_allowed() {
|
|
|
|
|
let d = make_detector();
|
|
|
|
|
let v = d.check(
|
|
|
|
|
"GET", "/", "app",
|
|
|
|
|
false, false, true,
|
|
|
|
|
"text/html",
|
|
|
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X) Safari/537.36",
|
|
|
|
|
0,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(v.action, ScannerAction::Allow);
|
|
|
|
|
assert_eq!(v.reason, "allowlist:host+browser");
|
|
|
|
|
}
|