Initial upload with full project structure
This commit is contained in:
103
app/src/confgen/formatter.py
Normal file
103
app/src/confgen/formatter.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
"""Output formatting with secret masking."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class OutputFormatter:
|
||||||
|
"""Formatter for configuration output with secret masking."""
|
||||||
|
|
||||||
|
SECRET_PATTERNS = [
|
||||||
|
"SECRET_",
|
||||||
|
"PASSWORD",
|
||||||
|
"API_KEY",
|
||||||
|
"API_SECRET",
|
||||||
|
"CREDENTIAL",
|
||||||
|
"TOKEN",
|
||||||
|
"PRIVATE_KEY",
|
||||||
|
"DB_PASSWORD",
|
||||||
|
"AUTH",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def format_value(self, value: Any, masked: bool = False) -> str:
|
||||||
|
"""Format a single value for display."""
|
||||||
|
if masked or self._is_secret(str(value)):
|
||||||
|
return "***"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def _is_secret(self, value: str) -> bool:
|
||||||
|
"""Check if a value appears to be a secret."""
|
||||||
|
value_upper = value.upper()
|
||||||
|
return any(pattern in value_upper for pattern in self.SECRET_PATTERNS)
|
||||||
|
|
||||||
|
def mask_content(self, content: str) -> str:
|
||||||
|
"""Mask secret values in content."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
patterns = [
|
||||||
|
(r"{{env\.([A-Z0-9_]+)}}", "***"),
|
||||||
|
(r"{{vault\.([A-Z0-9_]+)}}", "***"),
|
||||||
|
(r"(\b[A-Z0-9_]*SECRET[A-Z0-9_]*\b)", "***"),
|
||||||
|
(r"(\b[A-Z0-9_]*PASSWORD[A-Z0-9_]*\b)", "***"),
|
||||||
|
(r"(\b[A-Z0-9_]*API_KEY[A-Z0-9_]*\b)", "***"),
|
||||||
|
(r"(\b[A-Z0-9_]*TOKEN[A-Z0-9_]*\b)", "***"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern, _ in patterns:
|
||||||
|
content = re.sub(pattern, "***", content, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def format_dict(self, data: dict[str, Any], indent: int = 0) -> str:
|
||||||
|
"""Format a dictionary for display with secret masking."""
|
||||||
|
lines = []
|
||||||
|
prefix = " " * indent
|
||||||
|
|
||||||
|
for key, value in data.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
lines.append(f"{prefix}{key}:")
|
||||||
|
lines.append(self.format_dict(value, indent + 1))
|
||||||
|
elif isinstance(value, list):
|
||||||
|
lines.append(f"{prefix}{key}:")
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
lines.append(f"{prefix} -")
|
||||||
|
lines.append(self.format_dict(item, indent + 2))
|
||||||
|
else:
|
||||||
|
masked = self._is_secret(str(item))
|
||||||
|
lines.append(f"{prefix} - {self.format_value(item, masked)}")
|
||||||
|
else:
|
||||||
|
masked = self._is_secret(key) or self._is_secret(str(value))
|
||||||
|
lines.append(f"{prefix}{key}: {self.format_value(value, masked)}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def format_json(self, data: dict[str, Any], indent: int = 2) -> str:
|
||||||
|
"""Format data as JSON with secret masking."""
|
||||||
|
import json
|
||||||
|
|
||||||
|
masked_data = self._mask_dict(data)
|
||||||
|
return json.dumps(masked_data, indent=indent, ensure_ascii=False)
|
||||||
|
|
||||||
|
def _mask_dict(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Recursively mask secrets in a dictionary."""
|
||||||
|
masked = {}
|
||||||
|
for key, value in data.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
masked[key] = self._mask_dict(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
masked[key] = [
|
||||||
|
self._mask_dict(item) if isinstance(item, dict) else self._mask_value(item)
|
||||||
|
for item in value
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
masked[key] = self._mask_value(value)
|
||||||
|
return masked
|
||||||
|
|
||||||
|
def _mask_value(self, value: Any) -> Any:
|
||||||
|
"""Mask a single value if it's a secret."""
|
||||||
|
if isinstance(value, str) and self._is_secret(value):
|
||||||
|
return "***"
|
||||||
|
return value
|
||||||
Reference in New Issue
Block a user