Add sensitive variable redaction in history

This commit is contained in:
vesp 2026-01-13 11:27:05 +01:00
parent d7a336e524
commit ca52968626
5 changed files with 170 additions and 5 deletions

View File

@ -80,6 +80,7 @@ class HistoryEntry:
response: Optional[HttpResponse]
error: Optional[str] # Error message if request failed
id: str = None # Unique identifier for the entry
has_redacted_variables: bool = False # True if sensitive variables were redacted
def __post_init__(self):
"""Generate UUID if id not provided."""
@ -94,7 +95,8 @@ class HistoryEntry:
'timestamp': self.timestamp,
'request': self.request.to_dict(),
'response': self.response.to_dict() if self.response else None,
'error': self.error
'error': self.error,
'has_redacted_variables': self.has_redacted_variables
}
@classmethod
@ -105,7 +107,8 @@ class HistoryEntry:
timestamp=data['timestamp'],
request=HttpRequest.from_dict(data['request']),
response=HttpResponse.from_dict(data['response']) if data.get('response') else None,
error=data.get('error')
error=data.get('error'),
has_redacted_variables=data.get('has_redacted_variables', False)
)

View File

@ -127,3 +127,118 @@ class VariableSubstitution:
)
return new_request, all_undefined
@staticmethod
def substitute_excluding_variables(text: str, variables: Dict[str, str], excluded_vars: List[str]) -> Tuple[str, List[str]]:
"""
Replace {{variable_name}} with values, but skip excluded variables (leave as {{var}}).
Args:
text: The text containing variable placeholders
variables: Dictionary mapping variable names to their values
excluded_vars: List of variable names to skip (leave as placeholders)
Returns:
Tuple of (substituted_text, list_of_undefined_variables)
"""
if not text:
return text, []
undefined_vars = []
def replace_var(match):
var_name = match.group(1)
# Skip excluded variables - leave as {{var_name}}
if var_name in excluded_vars:
return match.group(0) # Return original {{var_name}}
# Substitute non-excluded variables
if var_name in variables:
value = variables[var_name]
# Check if value is empty/None - treat as undefined for warning purposes
if not value:
undefined_vars.append(var_name)
return ""
return value
else:
# Variable not defined - track it and replace with empty string
undefined_vars.append(var_name)
return ""
substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text)
return substituted, undefined_vars
@staticmethod
def substitute_request_excluding_sensitive(
request: HttpRequest,
environment: Environment,
sensitive_variables: List[str]
) -> Tuple[HttpRequest, Set[str], bool]:
"""
Substitute variables in request, but exclude sensitive variables (leave as {{var}}).
This is useful for saving requests to history without exposing sensitive values.
Sensitive variables remain in their {{variable_name}} placeholder form.
Args:
request: The HTTP request with variable placeholders
environment: The environment containing variable values
sensitive_variables: List of variable names to exclude from substitution
Returns:
Tuple of (redacted_request, set_of_undefined_variables, has_sensitive_vars)
- redacted_request: Request with only non-sensitive variables substituted
- set_of_undefined_variables: Variables that were referenced but not defined
- has_sensitive_vars: True if any sensitive variables were found and redacted
"""
all_undefined = set()
# Substitute URL (excluding sensitive variables)
new_url, url_undefined = VariableSubstitution.substitute_excluding_variables(
request.url, environment.variables, sensitive_variables
)
all_undefined.update(url_undefined)
# Substitute headers (both keys and values, excluding sensitive variables)
new_headers = {}
for key, value in request.headers.items():
new_key, key_undefined = VariableSubstitution.substitute_excluding_variables(
key, environment.variables, sensitive_variables
)
new_value, value_undefined = VariableSubstitution.substitute_excluding_variables(
value, environment.variables, sensitive_variables
)
all_undefined.update(key_undefined)
all_undefined.update(value_undefined)
new_headers[new_key] = new_value
# Substitute body (excluding sensitive variables)
new_body, body_undefined = VariableSubstitution.substitute_excluding_variables(
request.body, environment.variables, sensitive_variables
)
all_undefined.update(body_undefined)
# Create new HttpRequest with redacted values
redacted_request = HttpRequest(
method=request.method,
url=new_url,
headers=new_headers,
body=new_body,
syntax=request.syntax
)
# Check if any sensitive variables were actually used in the request
all_vars_in_request = set()
all_vars_in_request.update(VariableSubstitution.find_variables(request.url))
all_vars_in_request.update(VariableSubstitution.find_variables(request.body))
for key, value in request.headers.items():
all_vars_in_request.update(VariableSubstitution.find_variables(key))
all_vars_in_request.update(VariableSubstitution.find_variables(value))
# Determine if any sensitive variables were actually present
has_sensitive_vars = bool(
set(sensitive_variables) & all_vars_in_request
)
return redacted_request, all_undefined, has_sensitive_vars

View File

@ -62,6 +62,19 @@
</object>
</child>
<!-- Redaction Indicator (shown when sensitive variables were redacted) -->
<child>
<object class="GtkImage" id="redaction_indicator">
<property name="icon-name">dialog-information-symbolic</property>
<property name="tooltip-text">Sensitive variables were redacted from this history entry</property>
<property name="valign">center</property>
<property name="visible">False</property>
<style>
<class name="warning"/>
</style>
</object>
</child>
<!-- Delete Button -->
<child>
<object class="GtkButton" id="delete_button">

View File

@ -34,6 +34,7 @@ class HistoryItem(Gtk.Box):
url_label = Gtk.Template.Child()
timestamp_label = Gtk.Template.Child()
status_label = Gtk.Template.Child()
redaction_indicator = Gtk.Template.Child()
delete_button = Gtk.Template.Child()
request_headers_label = Gtk.Template.Child()
request_body_scroll = Gtk.Template.Child()
@ -77,6 +78,10 @@ class HistoryItem(Gtk.Box):
self.timestamp_label.set_text(timestamp_str)
# Show redaction indicator if sensitive variables were redacted
if self.entry.has_redacted_variables:
self.redaction_indicator.set_visible(True)
# Status
if self.entry.response:
status_text = f"{self.entry.response.status_code} {self.entry.response.status_text}"

View File

@ -569,12 +569,41 @@ class RosterWindow(Adw.ApplicationWindow):
if tab:
tab.response = response
# Create history entry (save the substituted request with actual values)
# Create history entry with redacted sensitive variables
# Determine which request to save to history
request_for_history = modified_request
has_redacted = False
if widget.selected_environment_id:
env = widget.get_selected_environment()
if env and widget.project_id:
# Get the project's sensitive variables list
projects = self.project_manager.load_projects()
sensitive_vars = []
for p in projects:
if p.id == widget.project_id:
sensitive_vars = p.sensitive_variables
break
# Create redacted request (only non-sensitive variables substituted)
if sensitive_vars:
from .variable_substitution import VariableSubstitution
request_for_history, _, has_redacted = VariableSubstitution.substitute_request_excluding_sensitive(
modified_request, env, sensitive_vars
)
else:
# No sensitive variables defined, use fully substituted request
request_for_history = substituted_request
elif env:
# Environment selected but no project - use fully substituted request
request_for_history = substituted_request
entry = HistoryEntry(
timestamp=datetime.now().isoformat(),
request=substituted_request,
request=request_for_history,
response=response,
error=error
error=error,
has_redacted_variables=has_redacted
)
self.history_manager.add_entry(entry)