Skip to content
Open
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
11 changes: 6 additions & 5 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ tokio = { version = "^1.52.3", default-features = false, features = [
"macros",
"fs",
"rt-multi-thread",
"net"
] }
tokio-stream = { version = "0.1.18", features = ["fs"] }
tokio-util = { version = "0.7.18" }
ipnet = "2.12.0"

# perf
hotpath = { version = "0.16.0", optional = true, features = [
Expand Down
2 changes: 1 addition & 1 deletion src/alerts/alert_traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,5 @@ pub trait AlertManagerTrait: Send + Sync {

#[async_trait]
pub trait CallableTarget {
async fn call(&self, payload: &Context);
async fn call(&self, tenant_id: &Option<String>, payload: &Context);
}
55 changes: 14 additions & 41 deletions src/alerts/alerts_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,7 @@ fn split_array_elements(value: &str) -> Result<Vec<&str>, String> {
.char_indices()
.nth(start)
.map_or(value.len(), |(b, _)| b);
let byte_end = value
.char_indices()
.nth(i)
.map_or(value.len(), |(b, _)| b);
let byte_end = value.char_indices().nth(i).map_or(value.len(), |(b, _)| b);
elements.push(&value[byte_start..byte_end]);
start = i + 1;
}
Expand Down Expand Up @@ -715,10 +712,7 @@ mod tests {
#[test]
fn test_sanitize_numeric_elements() {
assert_eq!(sanitize_array_elements("1, 2, 3").unwrap(), "1, 2, 3");
assert_eq!(
sanitize_array_elements("3.14, 2.71").unwrap(),
"3.14, 2.71"
);
assert_eq!(sanitize_array_elements("3.14, 2.71").unwrap(), "3.14, 2.71");
}

#[test]
Expand All @@ -732,10 +726,7 @@ mod tests {
#[test]
fn test_sanitize_bare_string_elements() {
assert_eq!(sanitize_array_elements("foo").unwrap(), "'foo'");
assert_eq!(
sanitize_array_elements("foo, bar").unwrap(),
"'foo', 'bar'"
);
assert_eq!(sanitize_array_elements("foo, bar").unwrap(), "'foo', 'bar'");
}

#[test]
Expand Down Expand Up @@ -775,8 +766,7 @@ mod tests {

#[test]
fn test_sanitize_multiple_quoted_with_commas() {
let result =
sanitize_array_elements("'hello, world', 'foo, bar'").unwrap();
let result = sanitize_array_elements("'hello, world', 'foo, bar'").unwrap();
assert_eq!(result, "'hello, world', 'foo, bar'");
}

Expand Down Expand Up @@ -852,50 +842,34 @@ mod tests {

#[test]
fn test_list_condition_expr_strips_outer_brackets() {
let result =
list_condition_expr("tags", &WhereConfigOperator::Equal, "[1, 2]").unwrap();
let result = list_condition_expr("tags", &WhereConfigOperator::Equal, "[1, 2]").unwrap();
assert_eq!(result, "\"tags\" = ARRAY[1, 2]");
}

#[test]
fn test_list_condition_expr_does_not_contain() {
let result = list_condition_expr(
"tags",
&WhereConfigOperator::DoesNotContain,
"foo",
)
.unwrap();
let result =
list_condition_expr("tags", &WhereConfigOperator::DoesNotContain, "foo").unwrap();
assert_eq!(result, "NOT array_has_all(\"tags\", ARRAY['foo'])");
}

#[test]
fn test_list_condition_expr_not_equal() {
let result =
list_condition_expr("tags", &WhereConfigOperator::NotEqual, "42").unwrap();
let result = list_condition_expr("tags", &WhereConfigOperator::NotEqual, "42").unwrap();
assert_eq!(result, "\"tags\" != ARRAY[42]");
}

#[test]
fn test_list_condition_expr_quoted_element_with_comma() {
let result = list_condition_expr(
"cities",
&WhereConfigOperator::Contains,
"'New York, NY'",
)
.unwrap();
assert_eq!(
result,
"array_has_all(\"cities\", ARRAY['New York, NY'])"
);
let result =
list_condition_expr("cities", &WhereConfigOperator::Contains, "'New York, NY'")
.unwrap();
assert_eq!(result, "array_has_all(\"cities\", ARRAY['New York, NY'])");
}

#[test]
fn test_list_condition_expr_rejects_injection() {
let result = list_condition_expr(
"tags",
&WhereConfigOperator::Contains,
"1] OR 1=1 --",
);
let result = list_condition_expr("tags", &WhereConfigOperator::Contains, "1] OR 1=1 --");
assert!(
result.is_err(),
"Injection via bracket characters must be rejected"
Expand All @@ -904,8 +878,7 @@ mod tests {

#[test]
fn test_list_condition_expr_unsupported_operator() {
let result =
list_condition_expr("tags", &WhereConfigOperator::GreaterThan, "1");
let result = list_condition_expr("tags", &WhereConfigOperator::GreaterThan, "1");
assert!(result.is_err());
assert!(result.unwrap_err().contains("not supported"));
}
Expand Down
24 changes: 20 additions & 4 deletions src/alerts/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub mod alert_structs;
pub mod alert_traits;
pub mod alert_types;
pub mod alerts_utils;
pub mod outbound_http_policy;
pub mod target;

pub use crate::alerts::alert_enums::{
Expand Down Expand Up @@ -619,7 +620,10 @@ impl AlertConfig {

for target_id in &self.targets {
let target = TARGETS.get_target_by_id(target_id, &self.tenant_id).await?;
trace!("Target (trigger_notifications)-\n{target:?}");
if let Err(e) = target.validate_outbound_policy().await {
tracing::error!("Target {target:?} failed alert policy check- {e}");
continue;
}
Comment on lines +623 to +626

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -B2 'struct (Auth|OtherWebHook|SlackWebHook|AlertManager)\b|enum TargetType\b' src/alerts/target.rs

Repository: parseablehq/parseable

Length of output: 782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target type definitions, masking helpers, and the log site in context.
sed -n '460,760p' src/alerts/target.rs
printf '\n--- LOG SITE ---\n'
sed -n '615,635p' src/alerts/mod.rs

printf '\n--- MASK USAGE ---\n'
rg -n "mask\\(|Debug for|impl std::fmt::Debug|impl Debug" src/alerts/target.rs src/alerts/mod.rs

Repository: parseablehq/parseable

Length of output: 9642


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '180,260p' src/alerts/target.rs

Repository: parseablehq/parseable

Length of output: 3278


Avoid Debug-printing Target here target:? includes TargetType and nested SlackWebHook/OtherWebHook/AlertManager/Auth fields, so webhook URLs, headers, and basic-auth credentials can end up in logs. Log target.mask() or only name/id instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/alerts/mod.rs` around lines 623 - 626, The alert policy validation error
log in the target processing path is exposing sensitive data by using Debug
formatting on Target. Update the tracing::error call in the
validate_outbound_policy failure branch to avoid target:? and instead log the
masked target via target.mask() or only safe identifiers like name/id, while
still including the validation error e for context.

target.call(context.clone());
}

Expand Down Expand Up @@ -976,6 +980,8 @@ pub enum AlertError {
Metadata(&'static str),
#[error("User is not authorized to run this query")]
Unauthorized,
#[error("Alert target outbound policy rejected request:{0}")]
OutboundPolicy(#[from] outbound_http_policy::OutboundPolicyError),
#[error("ActixError: {0}")]
Error(#[from] actix_web::Error),
#[error("DataFusion Error: {0}")]
Expand Down Expand Up @@ -1042,13 +1048,23 @@ impl actix_web::ResponseError for AlertError {
Self::Unimplemented(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotPresentInOSS(_) => StatusCode::BAD_REQUEST,
Self::MetastoreError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::OutboundPolicy(_) => StatusCode::BAD_REQUEST,
}
}

fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
actix_web::HttpResponse::build(self.status_code())
.insert_header(ContentType::plaintext())
.body(self.to_string())
match self {
Self::OutboundPolicy(_) => actix_web::HttpResponse::build(self.status_code())
.insert_header(ContentType::json())
.json(serde_json::json!({
"error": "Alert target blocked by outbound security policy",
"message": self.to_string(),
"hint": "Ask admin to allow this destination using the alert target policy."
})),
_ => actix_web::HttpResponse::build(self.status_code())
.insert_header(ContentType::plaintext())
.body(self.to_string()),
}
}
}

Expand Down
Loading
Loading