This document provides a comprehensive security analysis for displaying Azure CLI commands in azlin before execution. It identifies security risks, provides implementation guidance, and establishes testing requirements to prevent sensitive data exposure.
Risk Level: HIGH - Command display features can leak credentials, secrets, and sensitive configuration data if not properly sanitized.
Risk: Azure CLI commands contain sensitive parameters that must never be logged or displayed in plain text.
Attack Vectors:
- Terminal history leaking passwords
- Log files containing secrets
- Screen sharing exposing credentials
- Debug output in error reports
- Copy-paste of commands to public forums
Impact: Complete account compromise, unauthorized resource access, data breach
Risk: Displaying unsanitized commands may encourage users to copy-paste malicious commands.
Attack Vectors:
- Specially crafted resource names with shell metacharacters
- Command substitution in parameter values
- Multi-command injection via semicolons
Impact: Remote code execution, privilege escalation
Risk: Commands logged to files persist indefinitely and may be accessed by unauthorized users.
Attack Vectors:
- Log aggregation systems collecting secrets
- Backup systems preserving credentials
- Insecure log file permissions
- Log shipping to cloud services
Impact: Long-term credential exposure, compliance violations
Risk: Malicious escape sequences in displayed commands can manipulate terminal behavior.
Attack Vectors:
- ANSI escape codes in resource names
- Terminal title manipulation
- Clipboard hijacking via OSC sequences
Impact: UI spoofing, clipboard credential theft
Risk: Concurrent command display may interleave sensitive data from different contexts.
Attack Vectors:
- Thread-unsafe sanitization logic
- Shared sanitizer state across threads
- Time-of-check to time-of-use vulnerabilities
Impact: Cross-session data leakage
Risk: Command display may reveal infrastructure details to unauthorized observers.
Attack Vectors:
- Screen sharing during demos
- Shoulder surfing
- Terminal session recording
- Shared terminal multiplexer sessions
Impact: Reconnaissance for targeted attacks
SENSITIVE_AUTH_PARAMS = [
# Direct credentials
"--password",
"--admin-password",
"--client-secret",
# SSH keys
"--ssh-key-value",
"--ssh-key-values",
"--ssh-private-key-file",
"--generate-ssh-keys", # Caution: generates keys
# Certificates
"--certificate-data",
"--certificate-file",
# Service principals
"--service-principal",
"--tenant",
# Managed identity
"--identity",
]SENSITIVE_SECRET_PARAMS = [
# Key Vault
"--secrets",
"--secret",
# Storage
"--account-key",
"--connection-string",
"--sas-token",
# Database
"--administrator-login-password",
"--admin-user",
"--admin-password",
# Container registry
"--password",
"--docker-password",
# API tokens
"--token",
"--access-token",
]SENSITIVE_CONFIG_PARAMS = [
# Custom data (may contain secrets)
"--custom-data",
"--user-data",
"--cloud-init",
# Environment variables
"--environment-variables",
# Configuration strings
"--settings",
"--configuration",
]import re
from typing import Pattern
class AzureCommandSanitizer:
"""Sanitize Azure CLI commands for safe display."""
# Parameter patterns (case-insensitive)
SENSITIVE_PARAM_PATTERNS: dict[str, Pattern] = {
"password": re.compile(
r'--[a-z-]*password\s+["\']?([^\s"\']+)',
re.IGNORECASE
),
"secret": re.compile(
r'--[a-z-]*secret\s+["\']?([^\s"\']+)',
re.IGNORECASE
),
"key": re.compile(
r'--[a-z-]*(key|sas-token)\s+["\']?([^\s"\']+)',
re.IGNORECASE
),
"token": re.compile(
r'--[a-z-]*token\s+["\']?([^\s"\']+)',
re.IGNORECASE
),
"connection_string": re.compile(
r'--connection-string\s+["\']?([^\s"\']+)',
re.IGNORECASE
),
}
# Value patterns (detect secrets in values)
SECRET_VALUE_PATTERNS: dict[str, Pattern] = {
# Azure connection string format
"azure_connection_string": re.compile(
r'AccountKey=([A-Za-z0-9+/=]+)',
re.IGNORECASE
),
# Azure SAS token
"sas_token": re.compile(
r'(\?sv=\d{4}-\d{2}-\d{2}[^"\s]+)',
re.IGNORECASE
),
# Base64 encoded secrets (likely keys)
"base64_long": re.compile(
r'\b([A-Za-z0-9+/]{40,}={0,2})\b'
),
# JWT tokens
"jwt": re.compile(
r'(eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)'
),
}
REDACTED = "[REDACTED]"
@classmethod
def sanitize_command(cls, command: str) -> str:
"""Sanitize Azure CLI command for safe display.
Args:
command: Azure CLI command string
Returns:
Sanitized command with secrets replaced by [REDACTED]
Examples:
>>> sanitize_command("az vm create --admin-password MyPass123")
'az vm create --admin-password [REDACTED]'
"""
result = command
# Apply parameter-based patterns
for name, pattern in cls.SENSITIVE_PARAM_PATTERNS.items():
result = pattern.sub(
lambda m: m.group(0).split()[0] + " " + cls.REDACTED,
result
)
# Apply value-based patterns
for name, pattern in cls.SECRET_VALUE_PATTERNS.items():
result = pattern.sub(cls.REDACTED, result)
return result# Complete list of sensitive Azure CLI parameters
AZURE_SENSITIVE_PARAMS = {
# Authentication
"--password", "--admin-password", "--administrator-login-password",
"--client-secret", "--service-principal-secret",
# SSH
"--ssh-key-value", "--ssh-key-values", "--ssh-private-key-file",
# Storage & Keys
"--account-key", "--connection-string", "--sas-token",
"--primary-key", "--secondary-key", "--shared-access-key",
# Secrets
"--secret", "--secrets", "--secret-value",
# Tokens
"--token", "--access-token", "--refresh-token", "--bearer-token",
# Certificates
"--certificate-data", "--certificate-password", "--pfx-password",
# Database
"--db-password", "--sql-password",
# Custom data (may contain secrets)
"--custom-data", "--user-data", "--cloud-init",
# Docker
"--docker-password", "--registry-password",
# Environment variables (may contain secrets)
"--environment-variables", "--env",
}Implement multiple layers of protection:
- Input Sanitization: Sanitize at the point of command construction
- Display Sanitization: Sanitize before displaying to user
- Log Sanitization: Sanitize before writing to logs
- Output Sanitization: Sanitize in error messages
class CommandDisplayService:
"""Service for safely displaying Azure CLI commands."""
def __init__(self):
self.sanitizer = AzureCommandSanitizer()
self.log_sanitizer = LogSanitizer()
def display_command(self, command: str) -> None:
"""Display command with all sanitization layers applied."""
# Layer 1: Command-specific sanitization
safe_cmd = self.sanitizer.sanitize_command(command)
# Layer 2: General log sanitization
safe_cmd = self.log_sanitizer.sanitize(safe_cmd)
# Layer 3: Terminal escape sanitization
safe_cmd = self._sanitize_terminal_escapes(safe_cmd)
# Display
print(f"Executing: {safe_cmd}")
# Layer 4: Log with additional sanitization
self._log_command(safe_cmd)
def _sanitize_terminal_escapes(self, text: str) -> str:
"""Remove ANSI escape sequences and control characters."""
# Remove ANSI escape sequences
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
text = ansi_escape.sub('', text)
# Remove control characters except newline/tab
text = ''.join(char for char in text
if char in '\n\t' or ord(char) >= 32)
return text
def _log_command(self, command: str) -> None:
"""Log command with additional protection."""
# Use existing audit logger with sanitization
logger.info(f"Command: {command}")When in doubt, redact:
def should_redact_parameter(param_name: str) -> bool:
"""Conservative approach: redact if uncertain.
Returns True if parameter name suggests sensitive content.
"""
sensitive_keywords = [
'password', 'secret', 'key', 'token', 'credential',
'auth', 'private', 'cert', 'pfx', 'connection'
]
param_lower = param_name.lower()
return any(keyword in param_lower for keyword in sensitive_keywords)Use thread-local storage for sanitization state:
import threading
class ThreadSafeCommandSanitizer:
"""Thread-safe command sanitizer."""
_local = threading.local()
@classmethod
def sanitize(cls, command: str) -> str:
"""Thread-safe sanitization."""
# Each thread gets its own sanitizer instance
if not hasattr(cls._local, 'sanitizer'):
cls._local.sanitizer = AzureCommandSanitizer()
return cls._local.sanitizer.sanitize_command(command)import logging
from pathlib import Path
def setup_secure_logging():
"""Configure logging with security controls."""
log_file = Path.home() / ".azlin" / "commands.log"
# Ensure secure permissions
log_file.parent.mkdir(mode=0o700, exist_ok=True)
if log_file.exists():
log_file.chmod(0o600)
handler = logging.FileHandler(log_file)
handler.setLevel(logging.INFO)
# Use custom formatter with sanitization
formatter = SanitizingFormatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
handler.setFormatter(formatter)
return handler
class SanitizingFormatter(logging.Formatter):
"""Logging formatter that sanitizes sensitive data."""
def format(self, record: logging.LogRecord) -> str:
# Sanitize message before formatting
record.msg = LogSanitizer.sanitize(str(record.msg))
# Sanitize args
if record.args:
record.args = tuple(
LogSanitizer.sanitize(str(arg))
for arg in record.args
)
return super().format(record)# tests/unit/test_command_sanitization.py
import pytest
from azlin.security import AzureCommandSanitizer
class TestCommandSanitization:
"""Test Azure CLI command sanitization."""
@pytest.mark.parametrize("command,expected", [
# Passwords
(
"az vm create --admin-password MySecretPass123",
"az vm create --admin-password [REDACTED]"
),
# SSH keys
(
"az vm create --ssh-key-value 'ssh-rsa AAAAB3...'",
"az vm create --ssh-key-value [REDACTED]"
),
# Connection strings
(
"az storage account show-connection-string --connection-string 'DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=abcd1234'",
"az storage account show-connection-string --connection-string [REDACTED]"
),
# SAS tokens
(
"az storage blob url --account-name myaccount --sas-token '?sv=2021-01-01&ss=b&srt=sco&sp=rwdlac&se=2024-01-01'",
"az storage blob url --account-name myaccount --sas-token [REDACTED]"
),
# Custom data (may contain secrets)
(
"az vm create --custom-data '#!/bin/bash\\nPASS=secret'",
"az vm create --custom-data [REDACTED]"
),
# Multiple sensitive params
(
"az vm create --admin-password Pass123 --ssh-key-value 'ssh-rsa AAA'",
"az vm create --admin-password [REDACTED] --ssh-key-value [REDACTED]"
),
])
def test_sanitize_sensitive_parameters(self, command, expected):
"""Test that sensitive parameters are redacted."""
result = AzureCommandSanitizer.sanitize_command(command)
assert result == expected
def test_preserves_non_sensitive_parameters(self):
"""Test that non-sensitive parameters are preserved."""
command = "az vm create --name myvm --resource-group myrg --image Ubuntu2204"
result = AzureCommandSanitizer.sanitize_command(command)
assert result == command
def test_handles_quoted_values(self):
"""Test sanitization of quoted parameter values."""
command = 'az vm create --admin-password "My Pass 123"'
result = AzureCommandSanitizer.sanitize_command(command)
assert "My Pass 123" not in result
assert "[REDACTED]" in result
def test_thread_safety(self):
"""Test that sanitization is thread-safe."""
import concurrent.futures
commands = [
f"az vm create --admin-password Secret{i}"
for i in range(100)
]
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(
AzureCommandSanitizer.sanitize_command,
commands
))
# All secrets should be redacted
for result in results:
assert "Secret" not in result
assert "[REDACTED]" in result
def test_no_partial_redaction(self):
"""Test that entire secret values are redacted, not partial."""
command = "az vm create --admin-password VeryLongSecretPassword123456"
result = AzureCommandSanitizer.sanitize_command(command)
# Should not contain any part of the password
assert "VeryLong" not in result
assert "Password" not in result
assert "123456" not in result# tests/integration/test_command_display_security.py
import logging
from pathlib import Path
import pytest
from azlin.agentic.strategies.azure_cli import AzureCLIStrategy
from azlin.agentic.types import ExecutionContext, Intent
class TestCommandDisplaySecurity:
"""Integration tests for secure command display."""
def test_dry_run_sanitizes_sensitive_data(self, tmp_path):
"""Test that dry-run output sanitizes sensitive data."""
intent = Intent(
intent="Create VM with admin password",
parameters={
"vm_name": "test-vm",
"admin_password": "SuperSecret123!",
},
azlin_commands=[],
)
context = ExecutionContext(
intent=intent,
dry_run=True,
)
strategy = AzureCLIStrategy()
result = strategy.execute(context)
# Output should not contain password
assert "SuperSecret123!" not in result.output
assert "[REDACTED]" in result.output
def test_logs_do_not_contain_secrets(self, tmp_path, caplog):
"""Test that log files never contain sensitive data."""
# Configure logging to temp file
log_file = tmp_path / "test.log"
handler = logging.FileHandler(log_file)
logger = logging.getLogger("azlin")
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
try:
# Execute command with sensitive data
intent = Intent(
intent="Create VM",
parameters={
"vm_name": "test-vm",
"admin_password": "TopSecret999",
},
azlin_commands=[],
)
context = ExecutionContext(intent=intent)
strategy = AzureCLIStrategy()
# This will log the command
strategy.execute(context)
# Check log file
log_content = log_file.read_text()
assert "TopSecret999" not in log_content
finally:
logger.removeHandler(handler)
handler.close()
def test_error_messages_sanitized(self, monkeypatch):
"""Test that error messages don't leak secrets."""
import subprocess
def mock_run(*args, **kwargs):
# Simulate error that includes password
raise Exception("Authentication failed with password: SecretPass123")
monkeypatch.setattr(subprocess, "run", mock_run)
intent = Intent(
intent="Create VM",
parameters={"admin_password": "SecretPass123"},
azlin_commands=[],
)
context = ExecutionContext(intent=intent)
strategy = AzureCLIStrategy()
result = strategy.execute(context)
# Error should be sanitized
assert "SecretPass123" not in result.error
assert "[REDACTED]" in result.error# tests/security/test_data_leakage.py
class TestDataLeakagePrevention:
"""Security tests to prevent data leakage."""
def test_no_secrets_in_exception_repr(self):
"""Test that exception __repr__ doesn't leak secrets."""
try:
raise ValueError("Auth failed with password: Secret123")
except ValueError as e:
sanitized = LogSanitizer.sanitize_exception(e)
assert "Secret123" not in sanitized
def test_no_secrets_in_traceback(self):
"""Test that tracebacks are sanitized."""
import traceback
try:
password = "VerySecret123"
raise ValueError(f"Failed with {password}")
except ValueError:
tb = traceback.format_exc()
sanitized = LogSanitizer.sanitize(tb)
assert "VerySecret123" not in sanitized
def test_terminal_output_sanitized(self, capsys):
"""Test that terminal output is sanitized."""
from azlin.agentic.command_executor import CommandExecutor
executor = CommandExecutor()
result = executor.execute({
"command": "az vm create",
"args": ["--admin-password", "Secret123"]
})
# Capture output
captured = capsys.readouterr()
assert "Secret123" not in captured.out
assert "Secret123" not in captured.errAll command displays should be audit logged (without secrets):
class SecureCommandDisplay:
"""Secure command display with audit logging."""
def __init__(self):
self.audit_logger = AuditLogger()
self.sanitizer = AzureCommandSanitizer()
def display_before_execution(self, command: str, context: dict) -> None:
"""Display command before execution with full security controls."""
# Sanitize
safe_command = self.sanitizer.sanitize_command(command)
# Display to user
print(f"🔄 Executing: {safe_command}")
# Audit log
self.audit_logger.log(
event="COMMAND_DISPLAY",
details={
"command": safe_command,
"user": context.get("user"),
"timestamp": datetime.now(UTC).isoformat(),
}
)Warn users when sensitive parameters are used:
def warn_sensitive_parameters(command: str) -> None:
"""Warn user if command contains sensitive parameters."""
sensitive_params = [
p for p in AZURE_SENSITIVE_PARAMS
if p in command
]
if sensitive_params:
print("⚠️ WARNING: Command contains sensitive parameters:")
for param in sensitive_params:
print(f" - {param}")
print(" These will be redacted in logs and display.")Environment variables may also contain secrets:
def sanitize_environment(env: dict[str, str]) -> dict[str, str]:
"""Sanitize environment variables for display."""
sensitive_env_vars = {
'AZURE_CLIENT_SECRET',
'AZURE_PASSWORD',
'AZURE_STORAGE_KEY',
'ARM_CLIENT_SECRET',
}
return {
key: "[REDACTED]" if key in sensitive_env_vars else value
for key, value in env.items()
}-
Command Sanitization
- Implement AzureCommandSanitizer class
- Add parameter-based redaction
- Add value-based redaction (connection strings, tokens)
- Handle quoted and unquoted values
- Thread-safe implementation
-
Display Layer Security
- Sanitize before terminal output
- Remove ANSI escape sequences
- Sanitize progress indicators
- Sanitize error messages
-
Logging Security
- Implement SanitizingFormatter
- Configure secure log file permissions (0600)
- Audit log command displays
- Rotate logs securely
-
Testing
- Unit tests for all sensitive parameters
- Integration tests for dry-run mode
- Security tests for data leakage
- Thread safety tests
- Fuzzing tests for edge cases
-
Documentation
- User documentation on security features
- Developer guide for adding new parameters
- Security incident response procedures
-
Code Review
- Security review by team
- Penetration testing
- Third-party security audit (if required)
- Personal data in commands must be handled per GDPR
- Right to erasure applies to log files
- Data minimization principle applies to logging
If handling payment systems:
- No cardholder data in logs (Requirement 3.4)
- Render PAN unreadable (Requirement 3.4)
- Secure log files (Requirement 10.5)
For SOC 2 compliance:
- Audit trails of command execution
- Access controls on logs
- Encryption at rest for logs
-
Immediate Actions
- Rotate compromised credentials immediately
- Revoke leaked tokens/keys
- Audit access logs for unauthorized usage
-
Investigation
- Identify scope of exposure
- Determine if logs were accessed
- Review backup systems
-
Remediation
- Deploy fix to sanitization logic
- Update affected systems
- Notify affected users if required
-
Prevention
- Add test case for missed pattern
- Review similar code paths
- Enhanced monitoring
If vulnerability affects users:
- Follow responsible disclosure timeline
- Provide remediation guidance
- Update security documentation
src/azlin/log_sanitizer.pysrc/azlin/agentic/audit_logger.pytests/unit/test_auth_security.py
- Azure CLI Security Best Practices
- OWASP Logging Cheat Sheet
- CWE-532: Information Exposure Through Log Files
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2025-10-30 | Security Analysis | Initial security analysis document |
Classification: INTERNAL - Security Sensitive Review Frequency: Quarterly or after security incidents Next Review: 2026-01-30