Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
57355cc
add golang to mock server
bitterpanda63 Apr 21, 2026
9aa42b8
Add first version of golang firewall rule
bitterpanda63 Apr 21, 2026
d368391
Enable golang firewall rule in firewall/mod.rs
bitterpanda63 Apr 21, 2026
6b2d539
add e2e & test cases for golang firewall rule
bitterpanda63 Apr 21, 2026
6f2b2bb
Create a go_module_unescape.rs
bitterpanda63 Apr 21, 2026
2ed7da2
golang: Move from mod.rs -> parser.rs & add test cases
bitterpanda63 Apr 21, 2026
1c5d4b8
golang: min package age rewrites
bitterpanda63 Apr 21, 2026
d87190f
golang: Add match_http_response_payload_inspection_request
bitterpanda63 Apr 21, 2026
dac8b21
Merge branch 'main' into add-go-support
bitterpanda63 Apr 21, 2026
0368f6f
use make_response_uncacheable & update to use req_uri
bitterpanda63 Apr 22, 2026
327d7d7
delete empty tests.rs file
bitterpanda63 Apr 22, 2026
2908677
Use rama's percent_encoding instead of our own
bitterpanda63 Apr 22, 2026
7108dde
simplify parser.rs comments
bitterpanda63 Apr 22, 2026
ce94c5b
linting for golang pr
bitterpanda63 Apr 22, 2026
680cb89
Fix clippy issues
bitterpanda63 Apr 22, 2026
ef6cfb5
Merge branch 'main' into add-go-support
bitterpanda63 Apr 22, 2026
6cc6de7
Merge update after huge refactor
bitterpanda63 Apr 23, 2026
9d2523a
Merge branch 'main' into add-go-support
bitterpanda63 Apr 27, 2026
de66d5d
Fix merge
bitterpanda63 Apr 27, 2026
f2b5d4c
fix additonal merge conflicts with main refactors
bitterpanda63 Apr 27, 2026
bc163ad
Use SystemTimestamMilliseconds::MAX
bitterpanda63 Apr 27, 2026
4661138
Fix MAX
bitterpanda63 Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,56 @@ async fn fetch_permissions(req: Request) -> impl IntoResponse {
"rejected_packages": []
}
}),
_ => default_ecosystem_policy.clone(),
};

let golang_policy = match token {
"policy-block-golang" => json!({
"block_all_installs": true,
"request_installs": false,
"minimum_allowed_age_timestamp": null,
"exceptions": {
"allowed_packages": [],
"rejected_packages": []
}
}),
"policy-allow-gorilla-mux-golang" => json!({
"block_all_installs": false,
"request_installs": false,
"minimum_allowed_age_timestamp": null,
"exceptions": {
"allowed_packages": ["github.com/gorilla/mux"], // listed as malware in mock list
"rejected_packages": []
}
}),
"policy-reject-gin-golang" => json!({
"block_all_installs": false,
"request_installs": false,
"minimum_allowed_age_timestamp": null,
"exceptions": {
"allowed_packages": [],
"rejected_packages": ["github.com/gin-gonic/gin"]
}
}),
"policy-request-installs-golang" => json!({
"block_all_installs": false,
"request_installs": true,
"minimum_allowed_age_timestamp": null,
"exceptions": {
"allowed_packages": [],
"rejected_packages": []
}
}),
"policy-bypass-new-package-golang" => json!({
"block_all_installs": false,
"request_installs": false,
// Cutoff set to far future (year ~2286): released_on (year ~2255) <= cutoff → not blocked
"minimum_allowed_age_timestamp": 9_999_999_999_i64,
"exceptions": {
"allowed_packages": [],
"rejected_packages": []
}
}),
_ => default_ecosystem_policy,
};

Expand All @@ -404,7 +454,8 @@ async fn fetch_permissions(req: Request) -> impl IntoResponse {
"nuget": nuget_policy,
"chrome": chrome_policy,
"open_vsx": open_vsx_policy,
"skills_sh": skills_sh_policy
"skills_sh": skills_sh_policy,
"golang": golang_policy
}
}))
.into_response()
Expand Down
22 changes: 22 additions & 0 deletions proxy-bin-l7/src/client/mock_server/malware_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub(super) fn web_svc() -> impl Service<Request, Output = Response, Error = Infa
.with_get("malware_maven.json", malware_maven)
.with_get("malware_open_vsx.json", malware_open_vsx)
.with_get("malware_skills_sh.json", malware_skills_sh)
.with_get("malware_golang.json", malware_golang)
.with_get("releases/vscode.json", released_vscode)
.with_get("releases/open_vsx.json", released_open_vsx)
.with_get("releases/npm.json", released_npm)
Expand All @@ -35,6 +36,7 @@ pub(super) fn web_svc() -> impl Service<Request, Output = Response, Error = Infa
.with_get("releases/nuget.json", released_nuget)
.with_get("releases/skills_sh.json", released_skills_sh)
.with_get("releases/chrome.json", released_chrome)
.with_get("releases/golang.json", released_golang)
}

pub const FRESH_VSCODE_EXTENSION_PUBLISHER: &str = "newpublisher";
Expand Down Expand Up @@ -250,3 +252,23 @@ async fn malware_open_vsx() -> impl IntoResponse {
reason: Reason::Malware,
}])
}

pub const FRESH_GOLANG_MODULE_NAME: &str = "github.com/fresh-org/fresh-module";
pub const FRESH_GOLANG_MODULE_VERSION: &str = "1.0.0";
async fn released_golang() -> impl IntoResponse {
Json([ReleasedPackageData {
package_name: FRESH_GOLANG_MODULE_NAME.to_owned(),
version: PackageVersion::Semver(
PragmaticSemver::parse(FRESH_GOLANG_MODULE_VERSION).unwrap(),
),
released_on: 9_000_000_000,
}])
}

async fn malware_golang() -> impl IntoResponse {
Json([ListDataEntry {
package_name: "github.com/gorilla/mux".to_owned(),
version: PackageVersion::Semver(PragmaticSemver::new_semver(1, 8, 0)),
reason: Reason::Malware,
}])
}
207 changes: 207 additions & 0 deletions proxy-bin-l7/src/test/e2e/test_proxy/firewall_go.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
use rama::{
http::{BodyExtractExt as _, StatusCode, service::client::HttpClientExt as _},
telemetry::tracing,
};

use crate::{
client::mock_server::malware_list::{FRESH_GOLANG_MODULE_NAME, FRESH_GOLANG_MODULE_VERSION},
test::e2e,
};

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_malware_blocked() {
let runtime = e2e::runtime::get().await;
let client = runtime.client_with_http_proxy().await;

// github.com/gorilla/mux v1.8.0 is listed as malware in the mock list
let resp = client
.get("https://proxy.golang.org/github.com/gorilla/mux/@v/v1.8.0.zip")
.send()
.await
.unwrap();

assert_eq!(StatusCode::FORBIDDEN, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_safe_package_allowed() {
let runtime = e2e::runtime::get().await;
let client = runtime.client_with_http_proxy().await;

// gin is not in the malware list
let resp = client
.get("https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.zip")
.send()
.await
.unwrap();

assert_eq!(StatusCode::OK, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_non_zip_allowed() {
let runtime = e2e::runtime::get().await;
let client = runtime.client_with_http_proxy().await;

// .mod files are not intercepted — only .zip downloads are blocked
let resp = client
.get("https://proxy.golang.org/github.com/gorilla/mux/@v/v1.8.0.mod")
.send()
.await
.unwrap();

assert_eq!(StatusCode::OK, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_list_endpoint_allowed() {
let runtime = e2e::runtime::get().await;
let client = runtime.client_with_http_proxy().await;

// /@v/list is metadata, not intercepted
let resp = client
.get("https://proxy.golang.org/github.com/gorilla/mux/@v/list")
.send()
.await
.unwrap();

assert_eq!(StatusCode::OK, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_allows_different_version() {
let runtime = e2e::runtime::get().await;
let client = runtime.client_with_http_proxy().await;

// gorilla/mux v1.7.0 is not in the malware list (only v1.8.0 is)
let resp = client
.get("https://proxy.golang.org/github.com/gorilla/mux/@v/v1.7.0.zip")
.send()
.await
.unwrap();

assert_eq!(StatusCode::OK, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_package_allowed_by_endpoint_policy_exception() {
let runtime = e2e::runtime::spawn_with_agent_identity(
"policy-allow-gorilla-mux-golang",
"mock_device",
&[],
)
.await;
let client = runtime.client_with_http_proxy().await;

// gorilla/mux v1.8.0 is malware, but the allowed_packages exception overrides the malware check
let resp = client
.get("https://proxy.golang.org/github.com/gorilla/mux/@v/v1.8.0.zip")
.send()
.await
.unwrap();

assert_eq!(StatusCode::OK, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_package_blocked_by_endpoint_policy_block_all() {
let runtime =
e2e::runtime::spawn_with_agent_identity("policy-block-golang", "mock_device", &[]).await;
let client = runtime.client_with_http_proxy().await;

// gin is not malware, but block_all_installs blocks it
let resp = client
.get("https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.zip")
.send()
.await
.unwrap();

assert_eq!(StatusCode::FORBIDDEN, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_package_blocked_by_endpoint_policy_rejected_package() {
let runtime =
e2e::runtime::spawn_with_agent_identity("policy-reject-gin-golang", "mock_device", &[])
.await;
let client = runtime.client_with_http_proxy().await;

// gin is in rejected_packages — blocked even though it's not malware
let resp = client
.get("https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.zip")
.send()
.await
.unwrap();

assert_eq!(StatusCode::FORBIDDEN, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_package_blocked_by_endpoint_policy_request_installs() {
let runtime = e2e::runtime::spawn_with_agent_identity(
"policy-request-installs-golang",
"mock_device",
&[],
)
.await;
let client = runtime.client_with_http_proxy().await;

// gin is not malware, but request_installs requires approval for all installs
let resp = client
.get("https://proxy.golang.org/github.com/gin-gonic/gin/@v/v1.9.1.zip")
.send()
.await
.unwrap();

assert_eq!(StatusCode::FORBIDDEN, resp.status());
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_new_package_blocked() {
let runtime = e2e::runtime::get().await;
let client = runtime.client_with_http_proxy().await;

let module = FRESH_GOLANG_MODULE_NAME;
let ver = FRESH_GOLANG_MODULE_VERSION;
let url = format!("https://proxy.golang.org/{module}/@v/v{ver}.zip");

let resp = client.get(url).send().await.unwrap();

assert_eq!(StatusCode::FORBIDDEN, resp.status());

let payload = resp.try_into_string().await.unwrap();
assert!(
payload.to_lowercase().contains("24 hours") || payload.to_lowercase().contains("vetted"),
"expected blocked response to mention 24-hour vetting, got: {payload}"
);
}

#[tokio::test]
#[tracing_test::traced_test]
async fn test_go_new_package_not_blocked_via_policy_cutoff() {
let runtime = e2e::runtime::spawn_with_agent_identity(
"policy-bypass-new-package-golang",
"mock_device",
&[],
)
.await;
let client = runtime.client_with_http_proxy().await;

let module = FRESH_GOLANG_MODULE_NAME;
let ver = FRESH_GOLANG_MODULE_VERSION;
let url = format!("https://proxy.golang.org/{module}/@v/v{ver}.zip");

let resp = client.get(url).send().await.unwrap();

assert_eq!(StatusCode::OK, resp.status());
}
1 change: 1 addition & 0 deletions proxy-bin-l7/src/test/e2e/test_proxy/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod no_firewall;

mod firewall_chrome;
mod firewall_go;
mod firewall_maven;
mod firewall_npm;
mod firewall_nuget;
Expand Down
14 changes: 13 additions & 1 deletion proxy-lib/src/http/firewall/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ use crate::{
remote_app_passthrough_list::RemoteAppPassthroughList,
},
http::firewall::rule::{
DynRule, npm::min_package_age::MinPackageAge, pypi::min_package_age::MinPackageAgePyPI,
DynRule, golang::min_package_age::MinPackageAgeGolang, npm::min_package_age::MinPackageAge,
pypi::min_package_age::MinPackageAgePyPI,
},
storage::SyncCompactDataStorage,
utils::{env::network_service_identifier, token::AgentIdentity},
Expand Down Expand Up @@ -262,6 +263,17 @@ impl Firewall {
.await
.context("create block rule: open vsx")?
.into_dyn(),
self::rule::golang::RuleGolang::try_new(
guard.clone(),
layered_client.clone(),
data.clone(),
policy_evaluator.clone(),
remote_endpoint_config.clone(),
Some(MinPackageAgeGolang::new(notifier.clone())),
)
.await
.context("create block rule: golang")?
.into_dyn(),
self::rule::skills_sh::RuleSkillsSh::try_new(
guard,
layered_client,
Expand Down
Loading
Loading