mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Forward executor network policy decisions for auditing (#38670)
## What changed - Add a best-effort `network/policyDecision` notification for final domain and non-domain policy decisions made by executor-local proxies. - Validate notifications against the active process on the controller and emit audit events with controller-trusted session and execution metadata. - Reserve outbound RPC capacity so audit notifications cannot block control messages, and expose valid `chatgpt-account-id` header values for audit attribution. ## Testing - Cover notification serialization, proxy decision capture, executor-to-controller delivery, trusted metadata handling, and reserved RPC capacity. GitOrigin-RevId: a39f96a6b3d9401c03d54eaef5b9a6d3fe0da78b
This commit is contained in:
@@ -48,6 +48,8 @@ pub use mitm_hook::MitmHookConfig;
|
||||
pub use mitm_hook::MitmHookMatchConfig;
|
||||
pub use network_policy::NetworkDecision;
|
||||
pub use network_policy::NetworkDecisionSource;
|
||||
pub use network_policy::NetworkPolicyAuditEvent;
|
||||
pub use network_policy::NetworkPolicyAuditObserver;
|
||||
pub use network_policy::NetworkPolicyDecider;
|
||||
pub use network_policy::NetworkPolicyDeciderFuture;
|
||||
pub use network_policy::NetworkPolicyDecision;
|
||||
|
||||
@@ -27,6 +27,27 @@ pub enum NetworkProtocol {
|
||||
Socks5Udp,
|
||||
}
|
||||
|
||||
/// A completed network-policy audit decision without tenant or session identity.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NetworkPolicyAuditEvent {
|
||||
pub timestamp: String,
|
||||
pub scope: String,
|
||||
pub decision: String,
|
||||
pub source: String,
|
||||
pub reason: String,
|
||||
pub protocol: NetworkProtocol,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub method: Option<String>,
|
||||
pub client: Option<String>,
|
||||
pub policy_override: bool,
|
||||
}
|
||||
|
||||
/// Observes final network-policy decisions without delaying or altering enforcement.
|
||||
///
|
||||
/// Implementations must return immediately and treat notification delivery as best effort.
|
||||
pub type NetworkPolicyAuditObserver = Arc<dyn Fn(NetworkPolicyAuditEvent) + Send + Sync + 'static>;
|
||||
|
||||
impl NetworkProtocol {
|
||||
pub const fn as_policy_protocol(self) -> &'static str {
|
||||
match self {
|
||||
@@ -236,11 +257,12 @@ struct PolicyAuditEventArgs<'a> {
|
||||
|
||||
fn emit_policy_audit_event(state: &NetworkProxyState, args: PolicyAuditEventArgs<'_>) {
|
||||
let metadata = state.audit_metadata();
|
||||
let timestamp = audit_timestamp();
|
||||
tracing::event!(
|
||||
target: AUDIT_TARGET,
|
||||
tracing::Level::INFO,
|
||||
event.name = POLICY_DECISION_EVENT_NAME,
|
||||
event.timestamp = %audit_timestamp(),
|
||||
event.timestamp = %timestamp,
|
||||
conversation.id = metadata.conversation_id.as_deref(),
|
||||
app.version = metadata.app_version.as_deref(),
|
||||
auth_mode = metadata.auth_mode.as_deref(),
|
||||
@@ -262,6 +284,21 @@ fn emit_policy_audit_event(state: &NetworkProxyState, args: PolicyAuditEventArgs
|
||||
execution.id = args.execution_id,
|
||||
network.policy.override = args.policy_override,
|
||||
);
|
||||
if let Some(observer) = &state.policy_audit_observer {
|
||||
observer(NetworkPolicyAuditEvent {
|
||||
timestamp,
|
||||
scope: args.scope.to_string(),
|
||||
decision: args.decision.to_string(),
|
||||
source: args.source.to_string(),
|
||||
reason: args.reason.to_string(),
|
||||
protocol: args.protocol,
|
||||
host: args.server_address.to_string(),
|
||||
port: args.server_port,
|
||||
method: args.method.map(str::to_string),
|
||||
client: args.client_addr.map(str::to_string),
|
||||
policy_override: args.policy_override,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn audit_timestamp() -> String {
|
||||
@@ -633,6 +670,53 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn policy_audit_observer_receives_domain_and_non_domain_decisions() {
|
||||
let mut state = network_proxy_state_for_policy(NetworkProxyConfig::default());
|
||||
let (captured_tx, captured_rx) = std::sync::mpsc::channel();
|
||||
state.set_policy_audit_observer(Arc::new(move |event| {
|
||||
captured_tx
|
||||
.send(event)
|
||||
.expect("observer should capture the policy decision");
|
||||
}));
|
||||
let decider: Arc<dyn NetworkPolicyDecider> =
|
||||
Arc::new(|_request| async { NetworkDecision::Allow });
|
||||
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "example.com".to_string(),
|
||||
port: 80,
|
||||
environment_id: None,
|
||||
client_addr: None,
|
||||
method: None,
|
||||
command: None,
|
||||
exec_policy_hint: None,
|
||||
});
|
||||
evaluate_host_policy(&state, Some(&decider), &request)
|
||||
.await
|
||||
.expect("evaluate domain policy");
|
||||
emit_block_decision_audit_event(
|
||||
&state,
|
||||
BlockDecisionAuditEventArgs {
|
||||
source: NetworkDecisionSource::ModeGuard,
|
||||
reason: REASON_METHOD_NOT_ALLOWED,
|
||||
protocol: NetworkProtocol::Http,
|
||||
server_address: "unix-socket",
|
||||
server_port: 0,
|
||||
method: Some("POST"),
|
||||
client_addr: None,
|
||||
},
|
||||
);
|
||||
|
||||
let events: Vec<_> = captured_rx.try_iter().collect();
|
||||
assert_eq!(
|
||||
events
|
||||
.iter()
|
||||
.map(|event| (event.scope.as_str(), event.decision.as_str()))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![("domain", "allow"), ("non_domain", "deny")]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn evaluate_host_policy_emits_domain_event_for_decider_allow_override() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxyConfig::default());
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::mitm::MitmState;
|
||||
use crate::mitm_hook::HookEvaluation;
|
||||
use crate::mitm_hook::MitmHooksByHost;
|
||||
use crate::mitm_hook::evaluate_mitm_hooks;
|
||||
use crate::network_policy::NetworkPolicyAuditObserver;
|
||||
use crate::policy::Host;
|
||||
use crate::policy::is_loopback_host;
|
||||
use crate::policy::is_non_public_ip;
|
||||
@@ -232,6 +233,7 @@ pub struct NetworkProxyState {
|
||||
state: Arc<RwLock<ConfigState>>,
|
||||
reloader: Arc<dyn ConfigReloader>,
|
||||
blocked_request_observer: Arc<RwLock<Option<Arc<dyn BlockedRequestObserver>>>>,
|
||||
pub(crate) policy_audit_observer: Option<NetworkPolicyAuditObserver>,
|
||||
credential_broker: CredentialBroker,
|
||||
audit_metadata: NetworkProxyAuditMetadata,
|
||||
execution_attributions: Arc<Mutex<HashMap<String, ExecutionAttribution>>>,
|
||||
@@ -266,6 +268,7 @@ impl Clone for NetworkProxyState {
|
||||
state: self.state.clone(),
|
||||
reloader: self.reloader.clone(),
|
||||
blocked_request_observer: self.blocked_request_observer.clone(),
|
||||
policy_audit_observer: self.policy_audit_observer.clone(),
|
||||
credential_broker: self.credential_broker.clone(),
|
||||
audit_metadata: self.audit_metadata.clone(),
|
||||
execution_attributions: self.execution_attributions.clone(),
|
||||
@@ -351,6 +354,7 @@ impl NetworkProxyState {
|
||||
state: Arc::new(RwLock::new(state)),
|
||||
reloader,
|
||||
blocked_request_observer: Arc::new(RwLock::new(blocked_request_observer)),
|
||||
policy_audit_observer: None,
|
||||
audit_metadata,
|
||||
execution_attributions: Arc::new(Mutex::new(HashMap::new())),
|
||||
environment_id: None,
|
||||
@@ -413,6 +417,11 @@ impl NetworkProxyState {
|
||||
*observer = blocked_request_observer;
|
||||
}
|
||||
|
||||
/// Installs a best-effort observer for every final domain and non-domain policy decision.
|
||||
pub fn set_policy_audit_observer(&mut self, observer: NetworkPolicyAuditObserver) {
|
||||
self.policy_audit_observer = Some(observer);
|
||||
}
|
||||
|
||||
pub fn audit_metadata(&self) -> &NetworkProxyAuditMetadata {
|
||||
&self.audit_metadata
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user