feat(cli): add core system components for CLI hardening
Add signal handling, environment detection, and secrets management: - signals.zig: graceful Ctrl+C handling and signal management - environment.zig: user environment detection for telemetry - secrets.zig: secrets redaction for secure logging Improves CLI reliability and security posture.
This commit is contained in:
parent
8ae0875800
commit
524f440fe4
3 changed files with 684 additions and 0 deletions
272
cli/src/core/environment.zig
Normal file
272
cli/src/core/environment.zig
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Environment detection for smart defaults
|
||||
/// Detects local dev, container, CI, or production environments
|
||||
pub const Environment = enum {
|
||||
local, // Local development
|
||||
container, // Docker, Kubernetes, etc.
|
||||
ci, // CI/CD pipeline (GitHub Actions, GitLab CI, etc.)
|
||||
production, // Production deployment
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Auto-detect current environment
|
||||
pub fn detect() Self {
|
||||
// Check for CI environments first (highest priority)
|
||||
if (isCI()) return .ci;
|
||||
|
||||
// Check for container environment
|
||||
if (isContainer()) return .container;
|
||||
|
||||
// Check for production
|
||||
if (isProduction()) return .production;
|
||||
|
||||
// Default to local
|
||||
return .local;
|
||||
}
|
||||
|
||||
/// Check if running in CI environment
|
||||
fn isCI() bool {
|
||||
const ci_vars = &[_][]const u8{
|
||||
"CI",
|
||||
"GITHUB_ACTIONS",
|
||||
"GITLAB_CI",
|
||||
"JENKINS_URL",
|
||||
"CIRCLECI",
|
||||
"TRAVIS",
|
||||
"BUILDKITE",
|
||||
"DRONE",
|
||||
"CODEBUILD_BUILD_ID",
|
||||
};
|
||||
|
||||
for (ci_vars) |var_name| {
|
||||
if (std.posix.getenv(var_name)) |_| return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check if running in container
|
||||
fn isContainer() bool {
|
||||
// Check for .dockerenv file
|
||||
if (std.fs.accessAbsolute("/.dockerenv", .{})) {
|
||||
return true;
|
||||
} else |_| {}
|
||||
|
||||
// Check for Kubernetes
|
||||
if (std.posix.getenv("KUBERNETES_SERVICE_HOST")) |_| return true;
|
||||
|
||||
// Check for container-specific env vars
|
||||
if (std.posix.getenv("CONTAINER")) |_| return true;
|
||||
|
||||
// Check cgroup for container indicators
|
||||
if (checkCgroup()) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check cgroup for container indicators
|
||||
fn checkCgroup() bool {
|
||||
const cgroup_path = "/proc/self/cgroup";
|
||||
const file = std.fs.openFileAbsolute(cgroup_path, .{}) catch return false;
|
||||
defer file.close();
|
||||
|
||||
const content = file.readToEndAlloc(std.heap.c_allocator, 4096) catch return false;
|
||||
defer std.heap.c_allocator.free(content);
|
||||
|
||||
// Check for container indicators in cgroup
|
||||
if (std.mem.indexOf(u8, content, "docker") != null) return true;
|
||||
if (std.mem.indexOf(u8, content, "kubepods") != null) return true;
|
||||
if (std.mem.indexOf(u8, content, "containerd") != null) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Check if production environment
|
||||
fn isProduction() bool {
|
||||
const prod_vars = &[_][]const u8{
|
||||
"FETCH_ML_ENV",
|
||||
"ENV",
|
||||
"NODE_ENV",
|
||||
"RAILS_ENV",
|
||||
"APP_ENV",
|
||||
};
|
||||
|
||||
for (prod_vars) |var_name| {
|
||||
if (std.posix.getenv(var_name)) |val| {
|
||||
if (std.mem.eql(u8, val, "production") or
|
||||
std.mem.eql(u8, val, "prod"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get description of environment
|
||||
pub fn description(self: Self) []const u8 {
|
||||
return switch (self) {
|
||||
.local => "Local development",
|
||||
.container => "Container environment",
|
||||
.ci => "CI/CD pipeline",
|
||||
.production => "Production deployment",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Smart defaults configuration
|
||||
pub const SmartDefaults = struct {
|
||||
env: Environment,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init() Self {
|
||||
return .{
|
||||
.env = Environment.detect(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default host based on environment
|
||||
pub fn host(self: Self) []const u8 {
|
||||
return switch (self.env) {
|
||||
.local => "localhost",
|
||||
.container => "host.docker.internal",
|
||||
.ci => "localhost",
|
||||
.production => "0.0.0.0",
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default base path for experiments
|
||||
pub fn basePath(self: Self) []const u8 {
|
||||
return switch (self.env) {
|
||||
.local => "~/ml-experiments",
|
||||
.container => "/workspace/ml-experiments",
|
||||
.ci => "/tmp/ml-experiments",
|
||||
.production => "/var/lib/fetch_ml/experiments",
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default data directory
|
||||
pub fn dataDir(self: Self) []const u8 {
|
||||
return switch (self.env) {
|
||||
.local => "~/ml-data",
|
||||
.container => "/workspace/data",
|
||||
.ci => "/tmp/ml-data",
|
||||
.production => "/var/lib/fetch_ml/data",
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default config path
|
||||
pub fn configPath(self: Self) []const u8 {
|
||||
return switch (self.env) {
|
||||
.local => "~/.ml/config.toml",
|
||||
.container => "/workspace/.ml/config.toml",
|
||||
.ci => "/tmp/.ml/config.toml",
|
||||
.production => "/etc/fetch_ml/config.toml",
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default Redis address
|
||||
pub fn redisAddr(self: Self) []const u8 {
|
||||
return switch (self.env) {
|
||||
.local => "localhost:6379",
|
||||
.container => "redis:6379",
|
||||
.ci => "localhost:6379",
|
||||
.production => "redis:6379",
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default SSH key path
|
||||
pub fn sshKeyPath(self: Self) []const u8 {
|
||||
return switch (self.env) {
|
||||
.local => "~/.ssh/id_rsa",
|
||||
.container => "/workspace/.ssh/id_rsa",
|
||||
.ci => "/tmp/.ssh/id_rsa",
|
||||
.production => "/etc/fetch_ml/ssh/id_rsa",
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default worker count
|
||||
pub fn workerCount(self: Self) u32 {
|
||||
return switch (self.env) {
|
||||
.local => 2,
|
||||
.container => 2,
|
||||
.ci => 1, // Minimal for fast testing
|
||||
.production => @intCast(std.Thread.getCpuCount() catch 4),
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default poll interval (seconds)
|
||||
pub fn pollInterval(self: Self) u32 {
|
||||
return switch (self.env) {
|
||||
.local => 5,
|
||||
.container => 5,
|
||||
.ci => 1, // Fast polling for CI
|
||||
.production => 10,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default log level
|
||||
pub fn logLevel(self: Self) []const u8 {
|
||||
return switch (self.env) {
|
||||
.local => "info",
|
||||
.container => "info",
|
||||
.ci => "debug", // Verbose for debugging
|
||||
.production => "warn", // Quieter in production
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default verbosity (for CLI output)
|
||||
pub fn verbose(self: Self) bool {
|
||||
return switch (self.env) {
|
||||
.local => true,
|
||||
.container => false,
|
||||
.ci => true, // Verbose for CI logs
|
||||
.production => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get default JSON output mode
|
||||
pub fn jsonOutput(self: Self) bool {
|
||||
return switch (self.env) {
|
||||
.local => false,
|
||||
.container => true, // JSON for containerized environments
|
||||
.ci => true, // JSON for CI parsing
|
||||
.production => true,
|
||||
};
|
||||
}
|
||||
|
||||
/// Print environment info (for debugging)
|
||||
pub fn printInfo(self: Self, writer: anytype) !void {
|
||||
try writer.print("Environment: {s} ({s})\n", .{
|
||||
@tagName(self.env),
|
||||
self.env.description(),
|
||||
});
|
||||
try writer.print(" Host: {s}\n", .{self.host()});
|
||||
try writer.print(" Base Path: {s}\n", .{self.basePath()});
|
||||
try writer.print(" Config: {s}\n", .{self.configPath()});
|
||||
try writer.print(" Redis: {s}\n", .{self.redisAddr()});
|
||||
try writer.print(" Workers: {d}\n", .{self.workerCount()});
|
||||
try writer.print(" Poll Interval: {d}s\n", .{self.pollInterval()});
|
||||
try writer.print(" Log Level: {s}\n", .{self.logLevel()});
|
||||
}
|
||||
};
|
||||
|
||||
/// Convenience function to get smart defaults
|
||||
pub fn getSmartDefaults() SmartDefaults {
|
||||
return SmartDefaults.init();
|
||||
}
|
||||
|
||||
test "environment detection" {
|
||||
// Just verify the functions don't crash
|
||||
const env = Environment.detect();
|
||||
_ = env.description();
|
||||
|
||||
const smart = SmartDefaults.init();
|
||||
_ = smart.host();
|
||||
_ = smart.basePath();
|
||||
_ = smart.configPath();
|
||||
}
|
||||
165
cli/src/core/signals.zig
Normal file
165
cli/src/core/signals.zig
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Ctrl+C / SIGINT handling for graceful shutdown
|
||||
/// Implements UX contract semantics from cli-tui-ux-contract-v1.md
|
||||
pub const SignalHandler = struct {
|
||||
/// Current operation type for context-aware shutdown
|
||||
pub const Operation = enum {
|
||||
idle,
|
||||
queue, // Job submission
|
||||
dry_run, // Validation only
|
||||
watch, // Status watch mode
|
||||
sync, // File synchronization
|
||||
hash, // Dataset hashing
|
||||
other,
|
||||
};
|
||||
|
||||
var current_operation: Operation = .idle;
|
||||
var shutdown_requested: bool = false;
|
||||
var original_termios: ?std.posix.termios = null;
|
||||
|
||||
/// Initialize signal handler
|
||||
pub fn init() void {
|
||||
// Set up Ctrl+C handler
|
||||
const act = std.posix.Sigaction{
|
||||
.handler = .{ .handler = handleSignal },
|
||||
.mask = std.posix.empty_sigset,
|
||||
.flags = 0,
|
||||
};
|
||||
|
||||
std.posix.sigaction(std.posix.SIG.INT, &act, null) catch |err| {
|
||||
std.log.warn("Failed to set up SIGINT handler: {}", .{err});
|
||||
};
|
||||
|
||||
// Also handle SIGTERM for container environments
|
||||
std.posix.sigaction(std.posix.SIG.TERM, &act, null) catch |err| {
|
||||
std.log.warn("Failed to set up SIGTERM handler: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Signal handler callback
|
||||
fn handleSignal(sig: i32) callconv(.C) void {
|
||||
_ = sig;
|
||||
shutdown_requested = true;
|
||||
|
||||
// Context-aware behavior per UX contract
|
||||
switch (current_operation) {
|
||||
.dry_run => {
|
||||
// Immediate exit, no side effects
|
||||
std.log.info("Dry-run interrupted - exiting immediately", .{});
|
||||
std.process.exit(0);
|
||||
},
|
||||
.queue => {
|
||||
// Attempt to cancel submission, show status
|
||||
std.log.info("Queue interrupted - attempting to cancel...", .{});
|
||||
// Note: actual cancellation logic handled by caller
|
||||
},
|
||||
.watch => {
|
||||
// Exit watch mode gracefully
|
||||
std.log.info("Watch mode interrupted - exiting", .{});
|
||||
// Caller should detect shutdown_requested and exit cleanly
|
||||
},
|
||||
.sync => {
|
||||
// Stop after current file, preserve partial state
|
||||
std.log.info("Sync interrupted - stopping after current operation", .{});
|
||||
},
|
||||
.hash => {
|
||||
// Stop hashing, preserve partial results
|
||||
std.log.info("Hash operation interrupted", .{});
|
||||
},
|
||||
.idle, .other => {
|
||||
std.log.info("Interrupted - cleaning up...", .{});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Set current operation for context-aware handling
|
||||
pub fn setOperation(op: Operation) void {
|
||||
current_operation = op;
|
||||
}
|
||||
|
||||
/// Clear current operation
|
||||
pub fn clearOperation() void {
|
||||
current_operation = .idle;
|
||||
}
|
||||
|
||||
/// Check if shutdown was requested
|
||||
pub fn isShutdownRequested() bool {
|
||||
return shutdown_requested;
|
||||
}
|
||||
|
||||
/// Reset shutdown flag (for testing)
|
||||
pub fn reset() void {
|
||||
shutdown_requested = false;
|
||||
current_operation = .idle;
|
||||
}
|
||||
|
||||
/// Run cleanup and exit gracefully
|
||||
pub fn shutdown(exit_code: u8) noreturn {
|
||||
// Restore terminal state if modified
|
||||
if (original_termios) |termios| {
|
||||
_ = std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, &termios) catch {};
|
||||
}
|
||||
|
||||
std.log.info("Shutting down gracefully (exit code: {d})", .{exit_code});
|
||||
std.process.exit(exit_code);
|
||||
}
|
||||
|
||||
/// Wait for condition or shutdown request
|
||||
pub fn waitOrShutdown(condition: *bool, timeout_ms: u32) bool {
|
||||
const start = std.time.milliTimestamp();
|
||||
while (!condition.*) {
|
||||
if (shutdown_requested) return false;
|
||||
|
||||
const now = std.time.milliTimestamp();
|
||||
if (now - start > timeout_ms) return false;
|
||||
|
||||
std.time.sleep(100_000); // 100ms
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Print current status before exiting
|
||||
pub fn printStatus(writer: anytype) !void {
|
||||
try writer.print("\nInterrupted during: {s}\n", .{@tagName(current_operation)});
|
||||
try writer.writeAll("Current operation status preserved.\n");
|
||||
}
|
||||
};
|
||||
|
||||
/// Convenience functions
|
||||
pub fn init() void {
|
||||
SignalHandler.init();
|
||||
}
|
||||
|
||||
pub fn setOperation(op: SignalHandler.Operation) void {
|
||||
SignalHandler.setOperation(op);
|
||||
}
|
||||
|
||||
pub fn clearOperation() void {
|
||||
SignalHandler.clearOperation();
|
||||
}
|
||||
|
||||
pub fn isShutdownRequested() bool {
|
||||
return SignalHandler.isShutdownRequested();
|
||||
}
|
||||
|
||||
pub fn checkAndShutdown(exit_code: u8) void {
|
||||
if (SignalHandler.isShutdownRequested()) {
|
||||
SignalHandler.shutdown(exit_code);
|
||||
}
|
||||
}
|
||||
|
||||
/// RAII guard for operation context
|
||||
pub const OperationGuard = struct {
|
||||
pub fn init(op: SignalHandler.Operation) OperationGuard {
|
||||
setOperation(op);
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(_: OperationGuard) void {
|
||||
clearOperation();
|
||||
}
|
||||
};
|
||||
|
||||
/// Export for integration with main.zig
|
||||
pub const default_signal_handler = SignalHandler.init;
|
||||
247
cli/src/utils/secrets.zig
Normal file
247
cli/src/utils/secrets.zig
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
const std = @import("std");
|
||||
|
||||
/// Secrets redaction for secure logging
|
||||
/// Prevents accidental exposure of sensitive data in logs/output
|
||||
pub const SecretsFilter = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
patterns: std.ArrayList(Pattern),
|
||||
|
||||
const Pattern = struct {
|
||||
name: []const u8,
|
||||
regex: []const u8, // Simplified pattern matching
|
||||
prefix: []const u8,
|
||||
suffix: []const u8,
|
||||
};
|
||||
|
||||
const Self = @This();
|
||||
|
||||
/// Common secret patterns
|
||||
const DEFAULT_PATTERNS = &[_]Pattern{
|
||||
.{
|
||||
.name = "API Key",
|
||||
.regex = "",
|
||||
.prefix = "api_key",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "API Key (alt)",
|
||||
.regex = "",
|
||||
.prefix = "api-key",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "Authorization",
|
||||
.regex = "",
|
||||
.prefix = "Authorization: Bearer ",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "Password",
|
||||
.regex = "",
|
||||
.prefix = "password",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "Secret",
|
||||
.regex = "",
|
||||
.prefix = "secret",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "Token",
|
||||
.regex = "",
|
||||
.prefix = "token",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "Private Key",
|
||||
.regex = "",
|
||||
.prefix = "-----BEGIN",
|
||||
.suffix = "-----END",
|
||||
},
|
||||
.{
|
||||
.name = "AWS Access Key",
|
||||
.regex = "",
|
||||
.prefix = "AKIA",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "GitHub Token",
|
||||
.regex = "",
|
||||
.prefix = "ghp_",
|
||||
.suffix = "",
|
||||
},
|
||||
.{
|
||||
.name = "Slack Token",
|
||||
.regex = "",
|
||||
.prefix = "xoxb-",
|
||||
.suffix = "",
|
||||
},
|
||||
};
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Self {
|
||||
var patterns = std.ArrayList(Pattern).init(allocator);
|
||||
|
||||
// Add default patterns
|
||||
for (DEFAULT_PATTERNS) |p| {
|
||||
patterns.append(p) catch {};
|
||||
}
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.patterns = patterns,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.patterns.deinit();
|
||||
}
|
||||
|
||||
/// Add custom pattern
|
||||
pub fn addPattern(self: *Self, name: []const u8, prefix: []const u8) !void {
|
||||
try self.patterns.append(.{
|
||||
.name = name,
|
||||
.regex = "",
|
||||
.prefix = prefix,
|
||||
.suffix = "",
|
||||
});
|
||||
}
|
||||
|
||||
/// Scan and redact secrets from text
|
||||
pub fn redact(self: *Self, text: []const u8) ![]const u8 {
|
||||
// Simple prefix-based redaction
|
||||
var result = try self.allocator.dupe(u8, text);
|
||||
errdefer self.allocator.free(result);
|
||||
|
||||
for (self.patterns.items) |pattern| {
|
||||
result = try self.redactPattern(result, pattern);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Redact a specific pattern
|
||||
fn redactPattern(self: *Self, text: []const u8, pattern: Pattern) ![]const u8 {
|
||||
var result = std.ArrayList(u8).init(self.allocator);
|
||||
defer result.deinit();
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < text.len) {
|
||||
// Check for pattern prefix
|
||||
if (i + pattern.prefix.len <= text.len and
|
||||
std.mem.eql(u8, text[i..][0..pattern.prefix.len], pattern.prefix))
|
||||
{
|
||||
// Found pattern, write prefix and redaction marker
|
||||
try result.appendSlice(pattern.prefix);
|
||||
try result.appendSlice(" [REDACTED]");
|
||||
|
||||
// Skip until end of line or suffix
|
||||
i += pattern.prefix.len;
|
||||
const end = if (pattern.suffix.len > 0)
|
||||
std.mem.indexOf(u8, text[i..], pattern.suffix) orelse text.len
|
||||
else
|
||||
std.mem.indexOfScalar(u8, text[i..], '\n') orelse text.len;
|
||||
|
||||
i += end;
|
||||
if (pattern.suffix.len > 0 and i < text.len) {
|
||||
try result.appendSlice(pattern.suffix);
|
||||
i += pattern.suffix.len;
|
||||
}
|
||||
} else {
|
||||
try result.append(text[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return result.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Quick check if text might contain secrets
|
||||
pub fn mightContainSecrets(self: Self, text: []const u8) bool {
|
||||
for (self.patterns.items) |pattern| {
|
||||
if (std.mem.indexOf(u8, text, pattern.prefix)) |_| {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/// Global secrets filter for application-wide use
|
||||
var global_filter: ?SecretsFilter = null;
|
||||
|
||||
/// Initialize global secrets filter
|
||||
pub fn init(allocator: std.mem.Allocator) void {
|
||||
if (global_filter == null) {
|
||||
global_filter = SecretsFilter.init(allocator);
|
||||
}
|
||||
}
|
||||
|
||||
/// Redact secrets from text using global filter
|
||||
pub fn redact(text: []const u8) ![]const u8 {
|
||||
if (global_filter) |*filter| {
|
||||
return filter.redact(text);
|
||||
}
|
||||
return text; // No filter, return as-is
|
||||
}
|
||||
|
||||
/// Check if text might contain secrets
|
||||
pub fn mightContainSecrets(text: []const u8) bool {
|
||||
if (global_filter) |filter| {
|
||||
return filter.mightContainSecrets(text);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Safe print that redacts secrets
|
||||
pub fn safePrint(writer: anytype, format: []const u8, args: anytype) !void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&buf, format, args) catch {
|
||||
try writer.print(format, args);
|
||||
return;
|
||||
};
|
||||
|
||||
const redacted = redact(msg) catch msg;
|
||||
defer if (redacted.ptr != msg.ptr) {
|
||||
if (global_filter) |*f| f.allocator.free(redacted);
|
||||
};
|
||||
|
||||
try writer.writeAll(redacted);
|
||||
}
|
||||
|
||||
/// Secure logging function
|
||||
pub fn secureLog(comptime level: std.log.Level, comptime format: []const u8, args: anytype) void {
|
||||
var buf: [4096]u8 = undefined;
|
||||
const msg = std.fmt.bufPrint(&buf, format, args) catch {
|
||||
// Fallback to raw log if formatting fails
|
||||
std.log.log(level, format, args);
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if we need redaction
|
||||
if (mightContainSecrets(msg)) {
|
||||
const redacted = redact(msg) catch msg;
|
||||
defer if (redacted.ptr != msg.ptr) {
|
||||
if (global_filter) |*f| f.allocator.free(redacted);
|
||||
};
|
||||
std.log.log(level, "{s}", .{redacted});
|
||||
} else {
|
||||
std.log.log(level, "{s}", .{msg});
|
||||
}
|
||||
}
|
||||
|
||||
test "secrets redaction" {
|
||||
const allocator = std.testing.allocator;
|
||||
|
||||
var filter = SecretsFilter.init(allocator);
|
||||
defer filter.deinit();
|
||||
|
||||
// Test redaction
|
||||
const input = "api_key=secret123 password=mypass";
|
||||
const output = try filter.redact(input);
|
||||
defer allocator.free(output);
|
||||
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, "secret123") == null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, "mypass") == null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, output, "[REDACTED]") != null);
|
||||
}
|
||||
Loading…
Reference in a new issue