Усилить безопасность и надёжность выполнения операций
This commit is contained in:
@@ -11,6 +11,7 @@ pub enum ApprovalRequestStatus {
|
||||
#[default]
|
||||
Pending,
|
||||
Approved,
|
||||
Executing,
|
||||
Denied,
|
||||
Expired,
|
||||
Completed,
|
||||
|
||||
@@ -38,8 +38,8 @@ pub mod domain {
|
||||
UserSessionId, WorkspaceId,
|
||||
};
|
||||
pub use crate::observability::{
|
||||
InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod,
|
||||
UsageRollup,
|
||||
INVOCATION_PREVIEW_MAX_BYTES, InvocationLevel, InvocationLog, InvocationSource,
|
||||
InvocationStatus, UsagePeriod, UsageRollup, sanitize_invocation_preview,
|
||||
};
|
||||
pub use crate::operation::{
|
||||
ConfigExport, ConfirmationPolicy, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus,
|
||||
@@ -129,7 +129,8 @@ pub use ids::{
|
||||
UserSessionId, WorkspaceId,
|
||||
};
|
||||
pub use observability::{
|
||||
InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod, UsageRollup,
|
||||
INVOCATION_PREVIEW_MAX_BYTES, InvocationLevel, InvocationLog, InvocationSource,
|
||||
InvocationStatus, UsagePeriod, UsageRollup, sanitize_invocation_preview,
|
||||
};
|
||||
pub use operation::{
|
||||
ConfigExport, ConfirmationPolicy, ExecutionConfig, GeneratedDraft, GeneratedDraftStatus,
|
||||
|
||||
@@ -1,9 +1,68 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use serde_json::{Map, Value, json};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{AgentId, OperationId, WorkspaceId};
|
||||
|
||||
pub const INVOCATION_PREVIEW_MAX_BYTES: usize = 16 * 1024;
|
||||
|
||||
pub fn sanitize_invocation_preview(value: &Value) -> Value {
|
||||
let redacted = redact_sensitive_fields(value);
|
||||
let Ok(encoded) = serde_json::to_vec(&redacted) else {
|
||||
return Value::Null;
|
||||
};
|
||||
if encoded.len() <= INVOCATION_PREVIEW_MAX_BYTES {
|
||||
return redacted;
|
||||
}
|
||||
let preview = String::from_utf8_lossy(&encoded[..INVOCATION_PREVIEW_MAX_BYTES]).into_owned();
|
||||
json!({
|
||||
"truncated": true,
|
||||
"original_bytes": encoded.len(),
|
||||
"preview": preview,
|
||||
})
|
||||
}
|
||||
|
||||
fn redact_sensitive_fields(value: &Value) -> Value {
|
||||
match value {
|
||||
Value::Object(object) => Value::Object(
|
||||
object
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
let value = if is_sensitive_key(key) {
|
||||
Value::String("[REDACTED]".to_owned())
|
||||
} else {
|
||||
redact_sensitive_fields(value)
|
||||
};
|
||||
(key.clone(), value)
|
||||
})
|
||||
.collect::<Map<String, Value>>(),
|
||||
),
|
||||
Value::Array(items) => Value::Array(items.iter().map(redact_sensitive_fields).collect()),
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_sensitive_key(key: &str) -> bool {
|
||||
let normalized = key.to_ascii_lowercase().replace('-', "_");
|
||||
let compact = normalized
|
||||
.chars()
|
||||
.filter(char::is_ascii_alphanumeric)
|
||||
.collect::<String>();
|
||||
[
|
||||
"authorization",
|
||||
"cookie",
|
||||
"password",
|
||||
"passwd",
|
||||
"secret",
|
||||
"token",
|
||||
]
|
||||
.iter()
|
||||
.any(|sensitive| compact.contains(sensitive))
|
||||
|| ["apikey", "accesskey", "privatekey"]
|
||||
.iter()
|
||||
.any(|sensitive| compact.contains(sensitive))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InvocationSource {
|
||||
@@ -87,7 +146,10 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use super::{InvocationLevel, InvocationLog, InvocationSource, InvocationStatus, UsagePeriod};
|
||||
use super::{
|
||||
INVOCATION_PREVIEW_MAX_BYTES, InvocationLevel, InvocationLog, InvocationSource,
|
||||
InvocationStatus, UsagePeriod, sanitize_invocation_preview,
|
||||
};
|
||||
use crate::{AgentId, OperationId, WorkspaceId, ids::InvocationLogId};
|
||||
|
||||
fn timestamp(value: &str) -> OffsetDateTime {
|
||||
@@ -129,4 +191,33 @@ mod tests {
|
||||
|
||||
assert_eq!(value, "7d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacts_sensitive_fields_recursively() {
|
||||
let preview = sanitize_invocation_preview(&json!({
|
||||
"authorization": "Bearer secret",
|
||||
"nested": {
|
||||
"api_key": "key",
|
||||
"refreshToken": "token",
|
||||
"client_secret_value": "secret",
|
||||
"value": 42
|
||||
}
|
||||
}));
|
||||
|
||||
assert_eq!(preview["authorization"], "[REDACTED]");
|
||||
assert_eq!(preview["nested"]["api_key"], "[REDACTED]");
|
||||
assert_eq!(preview["nested"]["refreshToken"], "[REDACTED]");
|
||||
assert_eq!(preview["nested"]["client_secret_value"], "[REDACTED]");
|
||||
assert_eq!(preview["nested"]["value"], 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncates_large_previews() {
|
||||
let preview = sanitize_invocation_preview(&json!({
|
||||
"body": "x".repeat(INVOCATION_PREVIEW_MAX_BYTES * 2)
|
||||
}));
|
||||
|
||||
assert_eq!(preview["truncated"], true);
|
||||
assert!(preview["original_bytes"].as_u64().unwrap() > INVOCATION_PREVIEW_MAX_BYTES as u64);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user