working on runtime executions
This commit is contained in:
@@ -3,14 +3,50 @@
|
||||
//! These tests verify that secrets are NOT exposed in process environment
|
||||
//! or command-line arguments, ensuring secure secret passing via stdin.
|
||||
|
||||
use attune_worker::runtime::python::PythonRuntime;
|
||||
use attune_common::models::runtime::{InterpreterConfig, RuntimeExecutionConfig};
|
||||
use attune_worker::runtime::process::ProcessRuntime;
|
||||
use attune_worker::runtime::shell::ShellRuntime;
|
||||
use attune_worker::runtime::{ExecutionContext, Runtime};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn make_python_process_runtime(packs_base_dir: PathBuf) -> ProcessRuntime {
|
||||
let config = RuntimeExecutionConfig {
|
||||
interpreter: InterpreterConfig {
|
||||
binary: "python3".to_string(),
|
||||
args: vec!["-u".to_string()],
|
||||
file_extension: Some(".py".to_string()),
|
||||
},
|
||||
environment: None,
|
||||
dependencies: None,
|
||||
};
|
||||
let runtime_envs_dir = packs_base_dir.parent().unwrap_or(&packs_base_dir).join("runtime_envs");
|
||||
ProcessRuntime::new("python".to_string(), config, packs_base_dir, runtime_envs_dir)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_secrets_not_in_environ() {
|
||||
let runtime = PythonRuntime::new();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let runtime = make_python_process_runtime(tmp.path().to_path_buf());
|
||||
|
||||
// Inline Python code that checks environment for secrets
|
||||
let code = r#"
|
||||
import os, json
|
||||
|
||||
environ_str = str(os.environ)
|
||||
|
||||
# Secrets should NOT be in environment
|
||||
has_secret_in_env = 'super_secret_key_do_not_expose' in environ_str
|
||||
has_password_in_env = 'secret_pass_123' in environ_str
|
||||
has_secret_prefix = any(k.startswith('SECRET_') for k in os.environ)
|
||||
|
||||
result = {
|
||||
'secrets_in_environ': has_secret_in_env or has_password_in_env or has_secret_prefix,
|
||||
'environ_check': 'SECRET_' not in environ_str
|
||||
}
|
||||
print(json.dumps(result))
|
||||
"#;
|
||||
|
||||
let context = ExecutionContext {
|
||||
execution_id: 1,
|
||||
@@ -28,69 +64,36 @@ async fn test_python_secrets_not_in_environ() {
|
||||
},
|
||||
timeout: Some(10),
|
||||
working_dir: None,
|
||||
entry_point: "run".to_string(),
|
||||
code: Some(
|
||||
r#"
|
||||
import os
|
||||
|
||||
def run():
|
||||
# Check if secrets are in environment variables
|
||||
environ_str = str(os.environ)
|
||||
|
||||
# Secrets should NOT be in environment
|
||||
has_secret_in_env = 'super_secret_key_do_not_expose' in environ_str
|
||||
has_password_in_env = 'secret_pass_123' in environ_str
|
||||
has_secret_prefix = 'SECRET_API_KEY' in os.environ or 'SECRET_PASSWORD' in os.environ
|
||||
|
||||
# But they SHOULD be accessible via get_secret()
|
||||
api_key_accessible = get_secret('api_key') == 'super_secret_key_do_not_expose'
|
||||
password_accessible = get_secret('password') == 'secret_pass_123'
|
||||
|
||||
return {
|
||||
'secrets_in_environ': has_secret_in_env or has_password_in_env or has_secret_prefix,
|
||||
'api_key_accessible': api_key_accessible,
|
||||
'password_accessible': password_accessible,
|
||||
'environ_check': 'SECRET_' not in environ_str
|
||||
}
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
entry_point: "inline".to_string(),
|
||||
code: Some(code.to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::Json,
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
assert!(result.is_success(), "Execution should succeed");
|
||||
assert_eq!(
|
||||
result.exit_code, 0,
|
||||
"Execution should succeed. stderr: {}",
|
||||
result.stderr
|
||||
);
|
||||
|
||||
let result_data = result.result.unwrap();
|
||||
let result_obj = result_data.get("result").unwrap();
|
||||
let result_data = result.result.expect("Should have parsed JSON result");
|
||||
|
||||
// Critical security check: secrets should NOT be in environment
|
||||
assert_eq!(
|
||||
result_obj.get("secrets_in_environ").unwrap(),
|
||||
result_data.get("secrets_in_environ").unwrap(),
|
||||
&serde_json::json!(false),
|
||||
"SECURITY FAILURE: Secrets found in process environment!"
|
||||
);
|
||||
|
||||
// Verify secrets ARE accessible via secure method
|
||||
assert_eq!(
|
||||
result_obj.get("api_key_accessible").unwrap(),
|
||||
&serde_json::json!(true),
|
||||
"Secrets should be accessible via get_secret()"
|
||||
);
|
||||
assert_eq!(
|
||||
result_obj.get("password_accessible").unwrap(),
|
||||
&serde_json::json!(true),
|
||||
"Secrets should be accessible via get_secret()"
|
||||
);
|
||||
|
||||
// Verify no SECRET_ prefix in environment
|
||||
assert_eq!(
|
||||
result_obj.get("environ_check").unwrap(),
|
||||
result_data.get("environ_check").unwrap(),
|
||||
&serde_json::json!(true),
|
||||
"Environment should not contain SECRET_ prefix variables"
|
||||
);
|
||||
@@ -159,30 +162,47 @@ echo "SECURITY_PASS: Secrets not in environment but accessible via get_secret"
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
|
||||
// Check execution succeeded
|
||||
assert!(result.is_success(), "Execution should succeed");
|
||||
assert!(
|
||||
result.is_success(),
|
||||
"Execution should succeed. stderr: {}",
|
||||
result.stderr
|
||||
);
|
||||
assert_eq!(result.exit_code, 0, "Exit code should be 0");
|
||||
|
||||
// Verify security pass message
|
||||
assert!(
|
||||
result.stdout.contains("SECURITY_PASS"),
|
||||
"Security checks should pass"
|
||||
"Security checks should pass. stdout: {}",
|
||||
result.stdout
|
||||
);
|
||||
assert!(
|
||||
!result.stdout.contains("SECURITY_FAIL"),
|
||||
"Should not have security failures"
|
||||
"Should not have security failures. stdout: {}",
|
||||
result.stdout
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_secret_isolation_between_actions() {
|
||||
let runtime = PythonRuntime::new();
|
||||
async fn test_python_secrets_isolated_between_actions() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let runtime = make_python_process_runtime(tmp.path().to_path_buf());
|
||||
|
||||
// First action with secret A — read it from stdin
|
||||
let code1 = r#"
|
||||
import sys, json
|
||||
|
||||
# Read secrets from stdin (the process executor writes them as JSON on stdin)
|
||||
secrets_line = sys.stdin.readline().strip()
|
||||
secrets = json.loads(secrets_line) if secrets_line else {}
|
||||
print(json.dumps({'secret_a': secrets.get('secret_a')}))
|
||||
"#;
|
||||
|
||||
// First action with secret A
|
||||
let context1 = ExecutionContext {
|
||||
execution_id: 3,
|
||||
action_ref: "security.action1".to_string(),
|
||||
@@ -195,26 +215,36 @@ async fn test_python_secret_isolation_between_actions() {
|
||||
},
|
||||
timeout: Some(10),
|
||||
working_dir: None,
|
||||
entry_point: "run".to_string(),
|
||||
code: Some(
|
||||
r#"
|
||||
def run():
|
||||
return {'secret_a': get_secret('secret_a')}
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
entry_point: "inline".to_string(),
|
||||
code: Some(code1.to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::Json,
|
||||
};
|
||||
|
||||
let result1 = runtime.execute(context1).await.unwrap();
|
||||
assert!(result1.is_success());
|
||||
assert_eq!(
|
||||
result1.exit_code, 0,
|
||||
"First action should succeed. stderr: {}",
|
||||
result1.stderr
|
||||
);
|
||||
|
||||
// Second action with secret B — should NOT see secret A
|
||||
let code2 = r#"
|
||||
import sys, json
|
||||
|
||||
secrets_line = sys.stdin.readline().strip()
|
||||
secrets = json.loads(secrets_line) if secrets_line else {}
|
||||
print(json.dumps({
|
||||
'secret_a_leaked': secrets.get('secret_a') is not None,
|
||||
'secret_b_present': secrets.get('secret_b') == 'value_b'
|
||||
}))
|
||||
"#;
|
||||
|
||||
// Second action with secret B (should not see secret A)
|
||||
let context2 = ExecutionContext {
|
||||
execution_id: 4,
|
||||
action_ref: "security.action2".to_string(),
|
||||
@@ -227,42 +257,34 @@ def run():
|
||||
},
|
||||
timeout: Some(10),
|
||||
working_dir: None,
|
||||
entry_point: "run".to_string(),
|
||||
code: Some(
|
||||
r#"
|
||||
def run():
|
||||
# Should NOT see secret_a from previous action
|
||||
secret_a = get_secret('secret_a')
|
||||
secret_b = get_secret('secret_b')
|
||||
return {
|
||||
'secret_a_leaked': secret_a is not None,
|
||||
'secret_b_present': secret_b == 'value_b'
|
||||
}
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
entry_point: "inline".to_string(),
|
||||
code: Some(code2.to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::Json,
|
||||
};
|
||||
|
||||
let result2 = runtime.execute(context2).await.unwrap();
|
||||
assert!(result2.is_success());
|
||||
assert_eq!(
|
||||
result2.exit_code, 0,
|
||||
"Second action should succeed. stderr: {}",
|
||||
result2.stderr
|
||||
);
|
||||
|
||||
let result_data = result2.result.unwrap();
|
||||
let result_obj = result_data.get("result").unwrap();
|
||||
let result_data = result2.result.expect("Should have parsed JSON result");
|
||||
|
||||
// Verify secrets don't leak between actions
|
||||
assert_eq!(
|
||||
result_obj.get("secret_a_leaked").unwrap(),
|
||||
result_data.get("secret_a_leaked").unwrap(),
|
||||
&serde_json::json!(false),
|
||||
"Secret from previous action should not leak"
|
||||
);
|
||||
assert_eq!(
|
||||
result_obj.get("secret_b_present").unwrap(),
|
||||
result_data.get("secret_b_present").unwrap(),
|
||||
&serde_json::json!(true),
|
||||
"Current action's secret should be present"
|
||||
);
|
||||
@@ -270,43 +292,44 @@ def run():
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_empty_secrets() {
|
||||
let runtime = PythonRuntime::new();
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let runtime = make_python_process_runtime(tmp.path().to_path_buf());
|
||||
|
||||
// With no secrets, stdin should have nothing (or empty) — action should still work
|
||||
let code = r#"
|
||||
print("ok")
|
||||
"#;
|
||||
|
||||
let context = ExecutionContext {
|
||||
execution_id: 5,
|
||||
action_ref: "security.no_secrets".to_string(),
|
||||
parameters: HashMap::new(),
|
||||
env: HashMap::new(),
|
||||
secrets: HashMap::new(), // No secrets
|
||||
secrets: HashMap::new(),
|
||||
timeout: Some(10),
|
||||
working_dir: None,
|
||||
entry_point: "run".to_string(),
|
||||
code: Some(
|
||||
r#"
|
||||
def run():
|
||||
# get_secret should return None for non-existent secrets
|
||||
result = get_secret('nonexistent')
|
||||
return {'result': result}
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
entry_point: "inline".to_string(),
|
||||
code: Some(code.to_string()),
|
||||
code_path: None,
|
||||
runtime_name: Some("python".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
assert!(
|
||||
result.is_success(),
|
||||
"Should handle empty secrets gracefully"
|
||||
assert_eq!(
|
||||
result.exit_code, 0,
|
||||
"Should handle empty secrets gracefully. stderr: {}",
|
||||
result.stderr
|
||||
);
|
||||
assert!(
|
||||
result.stdout.contains("ok"),
|
||||
"Should produce expected output. stdout: {}",
|
||||
result.stdout
|
||||
);
|
||||
|
||||
let result_data = result.result.unwrap();
|
||||
let result_obj = result_data.get("result").unwrap();
|
||||
assert_eq!(result_obj.get("result").unwrap(), &serde_json::Value::Null);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -318,7 +341,7 @@ async fn test_shell_empty_secrets() {
|
||||
action_ref: "security.no_secrets".to_string(),
|
||||
parameters: HashMap::new(),
|
||||
env: HashMap::new(),
|
||||
secrets: HashMap::new(), // No secrets
|
||||
secrets: HashMap::new(),
|
||||
timeout: Some(10),
|
||||
working_dir: None,
|
||||
entry_point: "shell".to_string(),
|
||||
@@ -341,89 +364,155 @@ fi
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::default(),
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
assert!(
|
||||
result.is_success(),
|
||||
"Should handle empty secrets gracefully"
|
||||
"Should handle empty secrets gracefully. stderr: {}",
|
||||
result.stderr
|
||||
);
|
||||
assert!(
|
||||
result.stdout.contains("PASS"),
|
||||
"Should pass. stdout: {}",
|
||||
result.stdout
|
||||
);
|
||||
assert!(result.stdout.contains("PASS"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_python_special_characters_in_secrets() {
|
||||
let runtime = PythonRuntime::new();
|
||||
async fn test_process_runtime_secrets_not_in_environ() {
|
||||
// Verify ProcessRuntime (used for all runtimes now) doesn't leak secrets to env
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let pack_dir = tmp.path().join("testpack");
|
||||
let actions_dir = pack_dir.join("actions");
|
||||
std::fs::create_dir_all(&actions_dir).unwrap();
|
||||
|
||||
// Write a script that dumps environment
|
||||
std::fs::write(
|
||||
actions_dir.join("check_env.sh"),
|
||||
r#"#!/bin/bash
|
||||
if printenv | grep -q "SUPER_SECRET_VALUE"; then
|
||||
echo "FAIL: Secret leaked to environment"
|
||||
exit 1
|
||||
fi
|
||||
echo "PASS: No secrets in environment"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = RuntimeExecutionConfig {
|
||||
interpreter: InterpreterConfig {
|
||||
binary: "/bin/bash".to_string(),
|
||||
args: vec![],
|
||||
file_extension: Some(".sh".to_string()),
|
||||
},
|
||||
environment: None,
|
||||
dependencies: None,
|
||||
};
|
||||
let runtime = ProcessRuntime::new("shell".to_string(), config, tmp.path().to_path_buf(), tmp.path().join("runtime_envs"));
|
||||
|
||||
let context = ExecutionContext {
|
||||
execution_id: 7,
|
||||
action_ref: "security.special_chars".to_string(),
|
||||
action_ref: "testpack.check_env".to_string(),
|
||||
parameters: HashMap::new(),
|
||||
env: HashMap::new(),
|
||||
secrets: {
|
||||
let mut s = HashMap::new();
|
||||
s.insert("special_chars".to_string(), "test!@#$%^&*()".to_string());
|
||||
s.insert("with_newline".to_string(), "line1\nline2".to_string());
|
||||
s.insert("db_password".to_string(), "SUPER_SECRET_VALUE".to_string());
|
||||
s
|
||||
},
|
||||
timeout: Some(10),
|
||||
working_dir: None,
|
||||
entry_point: "run".to_string(),
|
||||
code: Some(
|
||||
r#"
|
||||
def run():
|
||||
special = get_secret('special_chars')
|
||||
newline = get_secret('with_newline')
|
||||
entry_point: "check_env.sh".to_string(),
|
||||
code: None,
|
||||
code_path: Some(actions_dir.join("check_env.sh")),
|
||||
runtime_name: Some("shell".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::default(),
|
||||
};
|
||||
|
||||
newline_char = chr(10)
|
||||
newline_parts = newline.split(newline_char) if newline else []
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
assert_eq!(
|
||||
result.exit_code, 0,
|
||||
"Check should pass. stdout: {}, stderr: {}",
|
||||
result.stdout, result.stderr
|
||||
);
|
||||
assert!(
|
||||
result.stdout.contains("PASS"),
|
||||
"Should confirm no secrets in env. stdout: {}",
|
||||
result.stdout
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
'special_correct': special == 'test!@#$%^&*()',
|
||||
'newline_has_two_parts': len(newline_parts) == 2,
|
||||
'newline_first_part': newline_parts[0] if len(newline_parts) > 0 else '',
|
||||
'newline_second_part': newline_parts[1] if len(newline_parts) > 1 else '',
|
||||
'special_len': len(special) if special else 0
|
||||
}
|
||||
"#
|
||||
.to_string(),
|
||||
),
|
||||
code_path: None,
|
||||
#[tokio::test]
|
||||
async fn test_python_process_runtime_secrets_not_in_environ() {
|
||||
// Same check but via ProcessRuntime with Python interpreter
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let pack_dir = tmp.path().join("testpack");
|
||||
let actions_dir = pack_dir.join("actions");
|
||||
std::fs::create_dir_all(&actions_dir).unwrap();
|
||||
|
||||
std::fs::write(
|
||||
actions_dir.join("check_env.py"),
|
||||
r#"
|
||||
import os, json
|
||||
|
||||
env_dump = str(os.environ)
|
||||
leaked = "TOP_SECRET_API_KEY" in env_dump
|
||||
print(json.dumps({"leaked": leaked}))
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let config = RuntimeExecutionConfig {
|
||||
interpreter: InterpreterConfig {
|
||||
binary: "python3".to_string(),
|
||||
args: vec!["-u".to_string()],
|
||||
file_extension: Some(".py".to_string()),
|
||||
},
|
||||
environment: None,
|
||||
dependencies: None,
|
||||
};
|
||||
let runtime = ProcessRuntime::new("python".to_string(), config, tmp.path().to_path_buf(), tmp.path().join("runtime_envs"));
|
||||
|
||||
let context = ExecutionContext {
|
||||
execution_id: 8,
|
||||
action_ref: "testpack.check_env".to_string(),
|
||||
parameters: HashMap::new(),
|
||||
env: HashMap::new(),
|
||||
secrets: {
|
||||
let mut s = HashMap::new();
|
||||
s.insert("api_key".to_string(), "TOP_SECRET_API_KEY".to_string());
|
||||
s
|
||||
},
|
||||
timeout: Some(10),
|
||||
working_dir: None,
|
||||
entry_point: "check_env.py".to_string(),
|
||||
code: None,
|
||||
code_path: Some(actions_dir.join("check_env.py")),
|
||||
runtime_name: Some("python".to_string()),
|
||||
max_stdout_bytes: 10 * 1024 * 1024,
|
||||
max_stderr_bytes: 10 * 1024 * 1024,
|
||||
parameter_delivery: attune_worker::runtime::ParameterDelivery::default(),
|
||||
parameter_format: attune_worker::runtime::ParameterFormat::default(),
|
||||
output_format: attune_worker::runtime::OutputFormat::Json,
|
||||
};
|
||||
|
||||
let result = runtime.execute(context).await.unwrap();
|
||||
assert!(
|
||||
result.is_success(),
|
||||
"Should handle special characters: {:?}",
|
||||
result.error
|
||||
assert_eq!(
|
||||
result.exit_code, 0,
|
||||
"Python env check should succeed. stderr: {}",
|
||||
result.stderr
|
||||
);
|
||||
|
||||
let result_data = result.result.unwrap();
|
||||
let result_obj = result_data.get("result").unwrap();
|
||||
|
||||
let result_data = result.result.expect("Should have parsed JSON result");
|
||||
assert_eq!(
|
||||
result_obj.get("special_correct").unwrap(),
|
||||
&serde_json::json!(true),
|
||||
"Special characters should be preserved"
|
||||
);
|
||||
assert_eq!(
|
||||
result_obj.get("newline_has_two_parts").unwrap(),
|
||||
&serde_json::json!(true),
|
||||
"Newline should split into two parts"
|
||||
);
|
||||
assert_eq!(
|
||||
result_obj.get("newline_first_part").unwrap(),
|
||||
&serde_json::json!("line1"),
|
||||
"First part should be 'line1'"
|
||||
);
|
||||
assert_eq!(
|
||||
result_obj.get("newline_second_part").unwrap(),
|
||||
&serde_json::json!("line2"),
|
||||
"Second part should be 'line2'"
|
||||
result_data.get("leaked").unwrap(),
|
||||
&serde_json::json!(false),
|
||||
"SECURITY FAILURE: Secret leaked to Python process environment!"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user