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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/goose/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ icu_locale = { version = "=2.1.1", default-features = false }
llama-cpp-sys-2 = { workspace = true, optional = true }
image = { version = "0.24.9", default-features = false, features = ["png", "jpeg", "gif", "webp"] }
subtle = { version = "2.5", default-features = false, features = ["std"] }
gethostname = "1.1.0"

[target.'cfg(target_os = "windows")'.dependencies]
winapi = { workspace = true }
Expand Down
10 changes: 9 additions & 1 deletion crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1719,7 +1719,15 @@ impl Agent {
.count();

let working_dir = session.working_dir.clone();
let reply_stream_span = tracing::info_span!(target: "goose::agents::agent", "reply_stream", trace_output = tracing::field::Empty, session.id = %session_config.id);
let reply_stream_span = tracing::info_span!(
target: "goose::agents::agent",
"reply_stream",
trace_output = tracing::field::Empty,
session.id = %session_config.id,
session.user = %crate::session_context::session_user(),
session.host = %crate::session_context::session_host(),
session.agent_type = "goose",
);
let inner = Box::pin(async_stream::try_stream! {
let mut turns_taken = 0u32;
let max_turns = session_config.max_turns.unwrap_or_else(|| {
Expand Down
14 changes: 10 additions & 4 deletions crates/goose/src/agents/tool_execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,18 @@ impl Agent {
.map_err(|_| anyhow::anyhow!("Confirmation channel closed for request {}", request.id))?;

if let Some(finding_id) = get_security_finding_id_from_results(&request.id, inspection_results) {
let action = match confirmation.permission {
Permission::AllowOnce | Permission::AlwaysAllow => "ALLOW",
_ => "BLOCK",
};
tracing::info!(
monotonic_counter.goose.prompt_injection_user_decisions = 1,
decision = ?confirmation.permission,
finding_id = %finding_id,
tool_request_id = %request.id,
"Prompt injection detection: user decision on command injection finding"
security.event_type = "user_decision",
security.action = action,
security.finding_id = %finding_id,
tool.request_id = %request.id,
user.decision = ?confirmation.permission,
"security finding: user decision"
);
}

Expand Down
123 changes: 83 additions & 40 deletions crates/goose/src/otel/otlp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,15 @@ pub fn promote_config_to_env(config: &crate::config::Config) {
}

fn create_resource() -> Resource {
use crate::session_context::{session_host, session_user};

let mut builder = Resource::builder_empty()
.with_attributes([
KeyValue::new("service.name", "goose"),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
KeyValue::new("service.namespace", "goose"),
KeyValue::new("host.name", session_host()),
KeyValue::new("user.name", session_user()),
])
.with_detector(Box::new(EnvResourceDetector::new()))
.with_detector(Box::new(TelemetryResourceDetector));
Expand Down Expand Up @@ -304,7 +308,12 @@ fn create_otlp_logs_layer() -> OtlpResult<OtlpLogsLayer> {
};

let bridge = OpenTelemetryTracingBridge::builder(&logger_provider)
.with_tracing_span_attributes(TracingSpanAttributes::allowlist(["session.id"]))
.with_tracing_span_attributes(TracingSpanAttributes::allowlist([
"session.id",
"session.user",
"session.host",
"session.agent_type",
]))
.build();
*LOGGER_PROVIDER.lock().unwrap_or_else(|e| e.into_inner()) = Some(logger_provider);

Expand Down Expand Up @@ -598,45 +607,79 @@ mod tests {
shutdown_otlp();
}

#[test_case(
&[],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.build();
"no env vars uses goose defaults"
)]
#[test_case(
&[("OTEL_SERVICE_NAME", "custom")],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.with_service_name("custom")
.build();
"OTEL_SERVICE_NAME overrides service.name"
)]
#[test_case(
&[("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod")],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.with_attribute(KeyValue::new("deployment.environment", "prod"))
.build();
"OTEL_RESOURCE_ATTRIBUTES adds custom attributes"
)]
#[test_case(
&[("OTEL_SERVICE_NAME", "custom"), ("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod")],
Resource::builder_empty()
.with_attributes([KeyValue::new("service.name", "goose"), KeyValue::new("service.version", env!("CARGO_PKG_VERSION")), KeyValue::new("service.namespace", "goose")])
.with_detector(Box::new(TelemetryResourceDetector))
.with_service_name("custom")
.with_attribute(KeyValue::new("deployment.environment", "prod"))
.build();
"OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES combine"
)]
fn test_create_resource(env: &[(&'static str, &'static str)], expected: Resource) {
let _guard = clear_otel_env(env);
assert_eq!(create_resource(), expected);
#[test]
fn test_create_resource_defaults() {
let _guard = clear_otel_env(&[]);
let resource = create_resource();
let attrs: Vec<_> = resource.iter().collect();
let get = |key: &str| {
attrs
.iter()
.find(|(k, _)| k.as_str() == key)
.map(|(_, v)| v.to_string())
};

assert_eq!(get("service.name").as_deref(), Some("goose"));
assert_eq!(
get("service.version").as_deref(),
Some(env!("CARGO_PKG_VERSION"))
);
assert_eq!(get("service.namespace").as_deref(), Some("goose"));
assert!(get("host.name").is_some(), "host.name should be set");
assert!(get("user.name").is_some(), "user.name should be set");
}

#[test]
fn test_create_resource_otel_service_name_overrides() {
let _guard = clear_otel_env(&[("OTEL_SERVICE_NAME", "custom")]);
let resource = create_resource();
let attrs: Vec<_> = resource.iter().collect();
let get = |key: &str| {
attrs
.iter()
.find(|(k, _)| k.as_str() == key)
.map(|(_, v)| v.to_string())
};

assert_eq!(get("service.name").as_deref(), Some("custom"));
assert_eq!(get("service.namespace").as_deref(), Some("goose"));
}

#[test]
fn test_create_resource_otel_resource_attributes() {
let _guard = clear_otel_env(&[("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod")]);
let resource = create_resource();
let attrs: Vec<_> = resource.iter().collect();
let get = |key: &str| {
attrs
.iter()
.find(|(k, _)| k.as_str() == key)
.map(|(_, v)| v.to_string())
};

assert_eq!(get("service.name").as_deref(), Some("goose"));
assert_eq!(get("deployment.environment").as_deref(), Some("prod"));
}

#[test]
fn test_create_resource_combined() {
let _guard = clear_otel_env(&[
("OTEL_SERVICE_NAME", "custom"),
("OTEL_RESOURCE_ATTRIBUTES", "deployment.environment=prod"),
]);
let resource = create_resource();
let attrs: Vec<_> = resource.iter().collect();
let get = |key: &str| {
attrs
.iter()
.find(|(k, _)| k.as_str() == key)
.map(|(_, v)| v.to_string())
};

assert_eq!(get("service.name").as_deref(), Some("custom"));
assert_eq!(get("deployment.environment").as_deref(), Some("prod"));
assert!(get("host.name").is_some());
assert!(get("user.name").is_some());
}

#[test_case(&[("RUST_LOG", "")], Level::INFO; "default is info")]
Expand Down
34 changes: 25 additions & 9 deletions crates/goose/src/security/adversary_inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,10 @@ impl ToolInspector for AdversaryInspector {
}

let tool_description = Self::format_tool_call(request);
let tool_call_name = match &request.tool_call {
Ok(tc) => tc.name.to_string(),
Err(_) => "unknown".to_string(),
};

tracing::debug!(
tool_request_id = %request.id,
Expand All @@ -397,9 +401,13 @@ impl ToolInspector for AdversaryInspector {
{
Ok((true, reason)) => {
tracing::debug!(
tool_request_id = %request.id,
reason = %reason,
"Adversary: ALLOW"
security.event_type = "adversary_detection",
security.action = "ALLOW",
security.confidence = 1.0_f32,
security.explanation = %reason,
tool.name = %tool_call_name,
tool.request_id = %request.id,
"adversary review: ALLOW"
);
results.push(InspectionResult {
tool_request_id: request.id.clone(),
Expand All @@ -412,9 +420,13 @@ impl ToolInspector for AdversaryInspector {
}
Ok((false, reason)) => {
tracing::warn!(
tool_request_id = %request.id,
reason = %reason,
"Adversary: BLOCK"
security.event_type = "adversary_detection",
security.action = "BLOCK",
security.confidence = 1.0_f32,
security.explanation = %reason,
tool.name = %tool_call_name,
tool.request_id = %request.id,
"adversary review: BLOCK"
);
results.push(InspectionResult {
tool_request_id: request.id.clone(),
Expand All @@ -427,9 +439,13 @@ impl ToolInspector for AdversaryInspector {
}
Err(e) => {
tracing::warn!(
tool_request_id = %request.id,
error = %e,
"Adversary inspector failed, allowing tool call (fail-open)"
security.event_type = "adversary_detection",
security.action = "ALLOW",
security.confidence = 0.0_f32,
security.explanation = %format!("error (fail-open): {}", e),
tool.name = %tool_call_name,
tool.request_id = %request.id,
"adversary review: error (fail-open)"
);
results.push(InspectionResult {
tool_request_id: request.id.clone(),
Expand Down
15 changes: 9 additions & 6 deletions crates/goose/src/security/egress_inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,12 +354,15 @@ impl ToolInspector for EgressInspector {

for dest in &destinations {
tracing::info!(
egress_kind = dest.kind.as_str(),
domain = dest.domain.as_str(),
destination = dest.destination.as_str(),
direction = direction.as_str(),
tool_name = name,
"egress destination detected"
security.event_type = "egress",
security.action = "LOG",
security.threat_type = "data_exfiltration",
network.destination = dest.destination.as_str(),
network.domain = dest.domain.as_str(),
network.egress_kind = dest.kind.as_str(),
network.direction = direction.as_str(),
tool.name = name,
"network egress detected"
);
}

Expand Down
42 changes: 25 additions & 17 deletions crates/goose/src/security/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,22 +162,26 @@ impl SecurityManager {
let tool_call_json =
serde_json::to_string(&tool_call).unwrap_or_else(|_| "{}".to_string());

let action = if above_threshold { "BLOCK" } else { "LOG" };

tracing::warn!(
monotonic_counter.goose.prompt_injection_finding = 1,
threat_type = "command_injection",
above_threshold = above_threshold,
tool_name = %tool_call.name,
tool_request_id = %tool_request.id,
tool_call_json = %tool_call_json,
confidence = analysis_result.confidence,
explanation = %sanitized_explanation,
finding_id = %finding_id,
threshold = config_threshold,
security.event_type = "prompt_injection_scan",
security.action = action,
security.confidence = analysis_result.confidence,
security.threshold = config_threshold,
security.above_threshold = above_threshold,
security.threat_type = "command_injection",
security.finding_id = %finding_id,
security.explanation = %sanitized_explanation,
tool.name = %tool_call.name,
tool.request_id = %tool_request.id,
tool.call_json = %tool_call_json,
"{}",
if above_threshold {
"Prompt injection detection: Current tool call flagged as malicious after security analysis (above threshold)"
"prompt injection scan: finding above threshold"
} else {
"Prompt injection detection: Security finding below threshold (logged but not blocking execution)"
"prompt injection scan: finding below threshold"
}
);
if above_threshold {
Expand All @@ -196,12 +200,16 @@ impl SecurityManager {

tracing::info!(
monotonic_counter.goose.prompt_injection_tool_call_passed = 1,
tool_name = %tool_call.name,
tool_request_id = %tool_request.id,
tool_call_json = %tool_call_json,
confidence = analysis_result.confidence,
explanation = %sanitized_explanation,
"Current tool call passed security analysis"
security.event_type = "prompt_injection_scan",
security.action = "ALLOW",
security.confidence = analysis_result.confidence,
security.threshold = config_threshold,
security.above_threshold = false,
security.threat_type = "command_injection",
tool.name = %tool_call.name,
tool.request_id = %tool_request.id,
tool.call_json = %tool_call_json,
"prompt injection scan: tool call passed"
);
}
}
Expand Down
19 changes: 10 additions & 9 deletions crates/goose/src/security/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,16 @@ impl PromptInjectionScanner {
self.combine_confidences(tool_result.confidence, context_result.ml_confidence);

tracing::info!(
tool_confidence = %tool_result.confidence,
context_confidence = ?context_result.ml_confidence,
final_confidence = %final_confidence,
used_command_ml = tool_result.ml_confidence.is_some(),
used_prompt_ml = context_result.ml_confidence.is_some(),
used_pattern_detection = tool_result.used_pattern_detection,
threshold = %threshold,
malicious = final_confidence >= threshold,
"Security analysis complete"
security.event_type = "prompt_injection_scan",
security.confidence = final_confidence,
security.threshold = threshold,
security.above_threshold = final_confidence >= threshold,
scanner.tool_confidence = tool_result.confidence,
scanner.context_confidence = ?context_result.ml_confidence,
scanner.used_command_ml = tool_result.ml_confidence.is_some(),
scanner.used_prompt_ml = context_result.ml_confidence.is_some(),
scanner.used_pattern_detection = tool_result.used_pattern_detection,
"prompt injection scan: analysis complete"
);

let final_result = DetailedScanResult {
Expand Down
14 changes: 14 additions & 0 deletions crates/goose/src/session_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ pub fn current_session_id() -> Option<String> {
SESSION_ID.try_with(|id| id.clone()).ok().flatten()
}

/// Local OS user running goose, shared by the OTLP `user.name` resource
/// attribute and the `session.user` span attribute so the two never drift.
pub fn session_user() -> String {
std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.unwrap_or_else(|_| "unknown".to_string())
}

/// Hostname of the machine running goose, shared by the OTLP `host.name`
/// resource attribute and the `session.host` span attribute.
pub fn session_host() -> String {
gethostname::gethostname().to_string_lossy().to_string()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading