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
13 changes: 11 additions & 2 deletions .machine_readable/6a2/STATE.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@
(state
(version . "0.1.0-dev")
(phase . "Pre-release verification")
(updated . "2026-04-16")
(updated . "2026-09-19")
(status . "active")

(local-ordered-checkpoint
(status . "Uncommitted strict-policy and unary-proxy corrections; not upstream or deployed")
(baseline . "19b343c1e9b7c61668c60887369783d12a486f41")
(targeted . "6 properties and 66 tests pass, plus real gateway to Julia JSON integration")
(full-suite . "seed 1: 12 properties, 263 tests, 15 failures; all 15 reproduce on baseline plus two necessary plugin compilation repairs")
(blockers . "Full suite red; Cowboy/Cowlib/Mint advisories; inactive plugin API warnings; container/TLS/reload/performance acceptance unrun")
(scope . "Matched path owns allowed verbs; empty globals deny unknown paths; regex ambiguity denies; raw bounded unary proxy; startup logging and ETS initialization repaired"))

(project
(name . "http-capability-gateway")
;; 19 Elixir modules implemented, 7 unit test files, 2 Zig FFI parsers,
;; 2 Idris2 ABI modules. Core gateway, policy pipeline, rate limiter,
;; circuit breaker, and proxy are functional. CRG grade C achieved.
;; Blockers: zero security tests, zero E2E tests, zero benchmarks.
;; Historical percentage/grade are not release acceptance. Current blockers
;; and executed test evidence are recorded in local-ordered-checkpoint above.
(completion . 55)
(crg-grade . "C")
(crg-date . "2026-04-04"))
Expand Down
14 changes: 12 additions & 2 deletions lib/http_capability_gateway/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ defmodule HttpCapabilityGateway.Application do
# Store policy table in application environment
Application.put_env(:http_capability_gateway, :policy_table, policy_table)

# Own hot-path tables for the application lifetime, before opening sockets.
HttpCapabilityGateway.K9Contract.init()
HttpCapabilityGateway.RateLimiter.init([])

# Start HTTP server and other children
port = Application.get_env(:http_capability_gateway, :port, 4000)

Expand Down Expand Up @@ -115,7 +119,8 @@ defmodule HttpCapabilityGateway.Application do
# refuses to start. We never silently downgrade an mTLS deployment to
# the forgeable header path.
defp http_listeners(port) do
http = {Plug.Cowboy, scheme: :http, plug: HttpCapabilityGateway.Gateway, options: [port: port]}
http =
{Plug.Cowboy, scheme: :http, plug: HttpCapabilityGateway.Gateway, options: [port: port]}

trust_source = Application.get_env(:http_capability_gateway, :trust_level_source, "header")

Expand Down Expand Up @@ -213,7 +218,11 @@ defmodule HttpCapabilityGateway.Application do
cond do
is_binary(catalog_root) ->
Logger.info("Catalog mode: building policy from BoJ cartridges", root: catalog_root)
compile_from_loader(fn -> PolicyLoader.load_from_boj_catalog(catalog_root) end, catalog_root)

compile_from_loader(
fn -> PolicyLoader.load_from_boj_catalog(catalog_root) end,
catalog_root
)

is_binary(policy_path) ->
Logger.info("Static mode: loading policy from file", path: policy_path)
Expand Down Expand Up @@ -309,6 +318,7 @@ defmodule HttpCapabilityGateway.Application do
"untrusted" => status_code
}
}

Application.put_env(:http_capability_gateway, :stealth_profiles, stealth_profiles)
Logger.info("Stealth mode enabled", status_code: status_code)

Expand Down
33 changes: 20 additions & 13 deletions lib/http_capability_gateway/logging.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,18 @@ defmodule HttpCapabilityGateway.Logging do
- `metadata` - Optional additional metadata map
"""
def log_request_received(request_id, conn, metadata \\ %{}) do
log_data = %{
event: "gateway.request.received",
request_id: request_id,
method: conn.method,
path: conn.request_path,
query_string: conn.query_string,
remote_ip: format_ip(conn.remote_ip),
user_agent: get_header(conn, "user-agent"),
trust_level: get_header(conn, "x-trust-level") || "untrusted"
}
|> Map.merge(metadata)
log_data =
%{
event: "gateway.request.received",
request_id: request_id,
method: conn.method,
path: conn.request_path,
query_string: conn.query_string,
remote_ip: format_ip(conn.remote_ip),
user_agent: get_header(conn, "user-agent"),
trust_level: get_header(conn, "x-trust-level") || "untrusted"
}
|> Map.merge(metadata)

Logger.info("Request received", log_data)

Expand Down Expand Up @@ -297,11 +298,17 @@ defmodule HttpCapabilityGateway.Logging do
- `metadata` - Optional metadata (service name, rules count, etc.)
"""
def log_policy_load(policy_path, result, metadata \\ %{}) do
result_tag =
case result do
:ok -> :ok
{:error, _} -> :error
end

log_data =
%{
event: "gateway.policy.load",
policy_path: policy_path,
result: elem(result, 0)
result: result_tag
}
|> Map.merge(metadata)

Expand All @@ -316,7 +323,7 @@ defmodule HttpCapabilityGateway.Logging do
:telemetry.execute(
[:http_capability_gateway, :policy, :load],
%{count: 1},
%{result: elem(result, 0)}
%{result: result_tag}
)
end

Expand Down
60 changes: 43 additions & 17 deletions lib/http_capability_gateway/plugins/webhook_hardener.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,34 @@

defmodule HttpCapabilityGateway.Plugins.WebhookHardener do
@moduledoc false
import Bitwise
@behaviour HttpCapabilityGateway.Plugin

@private_cidrs ["127.0.0.0/8", "::1/128", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16"]
@private_cidrs [
"127.0.0.0/8",
"::1/128",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16"
]

@impl true
def inspect_request(conn, opts) do
target = extract_target(conn)

cond do
!check_required_headers(conn, opts[:required_headers] || []) -> {:deny, conn, :missing_header}
target && byte_size(target) > (opts[:max_target_length] || 2048) -> {:deny, conn, :target_too_long}
target && ip_blocked?(target, opts[:blocked_cidrs] || @private_cidrs) -> {:deny, conn, :target_ip_blocked}
true -> {:allow, conn}
!check_required_headers(conn, opts[:required_headers] || []) ->
{:deny, conn, :missing_header}

target && byte_size(target) > (opts[:max_target_length] || 2048) ->
{:deny, conn, :target_too_long}

target && ip_blocked?(target, opts[:blocked_cidrs] || @private_cidrs) ->
{:deny, conn, :target_ip_blocked}

true ->
{:allow, conn}
end
end

Expand All @@ -31,7 +46,7 @@ defmodule HttpCapabilityGateway.Plugins.WebhookHardener do
end

defp check_required_headers(conn, required) do
Enum.all?(required, &Plug.Conn.get_req_header(conn, &1) != [])
Enum.all?(required, &(Plug.Conn.get_req_header(conn, &1) != []))
end

defp ip_blocked?(target, blocked) do
Expand All @@ -43,11 +58,14 @@ defmodule HttpCapabilityGateway.Plugins.WebhookHardener do

defp resolve_and_check(host, blocked) do
case try_parse_ip(host) do
{:ok, ip} -> in_blocked_range?(ip, blocked)
_ -> case :inet.gethostbyname(host) do
{:ok, {_, _, _, _, ip}} -> in_blocked_range?(ip, blocked)
_ -> false
end
{:ok, ip} ->
in_blocked_range?(ip, blocked)

_ ->
case :inet.gethostbyname(host) do
{:ok, {_, _, _, _, ip}} -> in_blocked_range?(ip, blocked)
_ -> false
end
end
end

Expand All @@ -65,12 +83,20 @@ defmodule HttpCapabilityGateway.Plugins.WebhookHardener do
defp in_cidr?(ip, cidr) do
with {:ok, net, mask} <- :inet.parse_cidr_address(cidr),
{:ok, net_int} <- to_int(net),
{:ok, ip_int} <- to_int(ip),
do: (ip_int &&& mask) == (net_int &&& mask),
else: _ -> false
{:ok, ip_int} <- to_int(ip) do
(ip_int &&& mask) == (net_int &&& mask)
else
_ -> false
end
end

defp to_int({a, b, c, d}), do: {:ok, (a <<< 24) ||| (b <<< 16) ||| (c <<< 8) ||| d}
defp to_int({a, b, c, d, e, f, g, h}), do: {:ok, (a <<< 120) ||| (b <<< 112) ||| (c <<< 104) ||| (d <<< 96) ||| (e <<< 88) ||| (f <<< 80) ||| (g <<< 72) ||| h}
defp to_int({a, b, c, d}), do: {:ok, a <<< 24 ||| b <<< 16 ||| c <<< 8 ||| d}

defp to_int({a, b, c, d, e, f, g, h}),
do:
{:ok,
a <<< 120 ||| b <<< 112 ||| c <<< 104 ||| d <<< 96 ||| e <<< 88 ||| f <<< 80 ||| g <<< 72 |||
h}

defp to_int(_), do: :error
end
4 changes: 2 additions & 2 deletions lib/http_capability_gateway/plugins/xml_rpc_shield.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ defmodule HttpCapabilityGateway.Plugins.XmlRpcShield do
@moduledoc false
@behaviour HttpCapabilityGateway.Plugin

@method_pattern ~r/<methodName>([^<]+)</methodName>/u
@method_pattern ~r{<methodName>([^<]+)</methodName>}u

@impl true
def inspect_request(conn, _opts) do
body = Plug.Conn.get_private(conn, :body)

case extract_method(body) do
nil -> :pass
"pingback.ping" -> {:allow, conn}
Expand Down
Loading
Loading