2026-10-09 12:49:07 +02:00

45 lines
1.7 KiB
GDScript

@tool
extends RefCounted
## Secret scrubber — redacts credentials from log/error output before
## returning it through the MCP transport. Applied to debugger_get_log
## and editor_get_console; never applied to
## script_read (source code should not be mutated).
# Three high-precision patterns that cover real secrets with minimal
# false-positives. The broad hex pattern ([A-Fa-f0-9]{32,}) is
# deliberately omitted — false-positives on Godot UIDs, asset hashes,
# and import checksums outweigh the marginal secret-detection gain.
# Add patterns here only when a concrete leak is found.
const PATTERNS: Array[String] = [
"(?i)(api[_-]?key|token|secret|password)\\s*[:=]\\s*[\\w\\.\\-]+",
"sk-[A-Za-z0-9]{20,}",
"(?i)bearer\\s+[A-Za-z0-9\\.\\-_]+",
]
## Compiled once at script load.
static var _compiled_patterns: Array[RegEx] = _compile_patterns()
static func _compile_patterns() -> Array[RegEx]:
var out: Array[RegEx] = []
for pattern in PATTERNS:
var regex := RegEx.new()
regex.compile(pattern)
out.append(regex)
return out
## Scrub secrets from text. Returns {text: String, redaction_count: int}.
## When redaction_count > 0 and source is non-empty, emits a push_warning
## so developers can correlate [REDACTED] appearances with the scrubber.
static func scrub(text: String, source: String = "") -> Dictionary:
var out := text
var redaction_count := 0
for regex in _compiled_patterns:
var matches := regex.search_all(out)
redaction_count += matches.size()
out = regex.sub(out, "[REDACTED]", true)
if redaction_count > 0 and not source.is_empty():
push_warning("[Scrubber] %d redactions applied to %s" % [redaction_count, source])
return {"text": out, "redaction_count": redaction_count}