///|
/// Severity assigned to a terminal-output finding.
pub(all) enum Severity {
Info
Warning
Dangerous
} derive(Eq, Debug)
///|
/// Returns a stable display label.
pub fn Severity::label(self : Severity) -> String {
match self {
Info => "info"
Warning => "warning"
Dangerous => "dangerous"
}
}
///|
/// Category of a terminal control sequence or control character.
pub(all) enum FindingKind {
StyleSequence
CursorControl
ScreenControl
ModeChange
TerminalQuery
WindowControl
TitleChange
Hyperlink
ClipboardWrite
WorkingDirectory
Notification
ShellIntegration
FileTransfer
PaletteChange
CharacterSet
DeviceControl
ControlCharacter
TruncatedSequence
OversizedSequence
UnknownEscape
} derive(Eq, Debug)
///|
/// Returns a stable machine-friendly category name.
pub fn FindingKind::label(self : FindingKind) -> String {
match self {
StyleSequence => "style_sequence"
CursorControl => "cursor_control"
ScreenControl => "screen_control"
ModeChange => "mode_change"
TerminalQuery => "terminal_query"
WindowControl => "window_control"
TitleChange => "title_change"
Hyperlink => "hyperlink"
ClipboardWrite => "clipboard_write"
WorkingDirectory => "working_directory"
Notification => "notification"
ShellIntegration => "shell_integration"
FileTransfer => "file_transfer"
PaletteChange => "palette_change"
CharacterSet => "character_set"
DeviceControl => "device_control"
ControlCharacter => "control_character"
TruncatedSequence => "truncated_sequence"
OversizedSequence => "oversized_sequence"
UnknownEscape => "unknown_escape"
}
}
///|
/// One item removed from or observed in terminal output.
pub struct Finding {
kind : FindingKind
severity : Severity
offset : Int
sequence : String
message : String
} derive(Eq, Debug)
///|
pub fn Finding::kind(self : Finding) -> FindingKind {
self.kind
}
///|
pub fn Finding::severity(self : Finding) -> Severity {
self.severity
}
///|
pub fn Finding::offset(self : Finding) -> Int {
self.offset
}
///|
pub fn Finding::sequence(self : Finding) -> String {
self.sequence
}
///|
pub fn Finding::message(self : Finding) -> String {
self.message
}
///|
/// Returns the captured sequence with common control bytes made visible.
pub fn Finding::sequence_preview(self : Finding) -> String {
visible_sequence(self.sequence)
}
///|
/// Controls which harmless terminal features may pass through.
pub struct Policy {
allow_sgr : Bool
allow_hyperlinks : Bool
allow_newline : Bool
allow_tab : Bool
max_sequence_length : Int
} derive(Eq, Debug)
///|
/// Creates a strict policy suitable for untrusted logs and AI tool output.
pub fn Policy::strict() -> Policy {
{
allow_sgr: false,
allow_hyperlinks: false,
allow_newline: true,
allow_tab: true,
max_sequence_length: 4096,
}
}
///|
/// Creates a policy that keeps SGR color and emphasis sequences.
pub fn Policy::styled() -> Policy {
{
allow_sgr: true,
allow_hyperlinks: false,
allow_newline: true,
allow_tab: true,
max_sequence_length: 4096,
}
}
///|
/// Creates a custom policy.
pub fn Policy::new(
allow_sgr? : Bool = false,
allow_hyperlinks? : Bool = false,
allow_newline? : Bool = true,
allow_tab? : Bool = true,
max_sequence_length? : Int = 4096,
) -> Policy {
{
allow_sgr,
allow_hyperlinks,
allow_newline,
allow_tab,
max_sequence_length: if max_sequence_length < 2 {
2
} else {
max_sequence_length
},
}
}
///|
/// Result of sanitizing one string.
pub struct ScanResult {
text : String
findings : Array[Finding]
removed_count : Int
} derive(Eq, Debug)
///|
pub fn ScanResult::text(self : ScanResult) -> String {
self.text
}
///|
pub fn ScanResult::findings(self : ScanResult) -> Array[Finding] {
self.findings.copy()
}
///|
pub fn ScanResult::removed_count(self : ScanResult) -> Int {
self.removed_count
}
///|
pub fn ScanResult::is_safe(self : ScanResult) -> Bool {
self.findings.all(finding => finding.severity != Dangerous)
}
///|
pub fn ScanResult::count_severity(
self : ScanResult,
severity : Severity,
) -> Int {
self.findings.fold(init=0, fn(total, finding) {
if finding.severity == severity {
total + 1
} else {
total
}
})
}
///|
/// Counts dangerous findings.
pub fn ScanResult::dangerous_count(self : ScanResult) -> Int {
self.count_severity(Dangerous)
}
///|
/// Counts warning findings.
pub fn ScanResult::warning_count(self : ScanResult) -> Int {
self.count_severity(Warning)
}
///|
/// Counts informational findings.
pub fn ScanResult::info_count(self : ScanResult) -> Int {
self.count_severity(Info)
}
///|
/// Computes a small risk score for dashboards and CI gates.
pub fn ScanResult::risk_score(self : ScanResult) -> Int {
self.dangerous_count() * 10 + self.warning_count() * 3 + self.info_count()
}
///|
/// Returns a stable coarse risk label.
pub fn ScanResult::risk_level(self : ScanResult) -> String {
if self.dangerous_count() > 0 {
"high"
} else if self.warning_count() > 0 {
"medium"
} else if self.info_count() > 0 {
"low"
} else {
"clean"
}
}
///|
/// Returns a stable action label for automation and CI integration.
pub fn ScanResult::recommended_action(self : ScanResult) -> String {
self.recommended_decision().label()
}
///|
/// Renders a compact human-readable audit report.
pub fn ScanResult::audit_report(self : ScanResult) -> String {
let mut report = "MoonAnsi Guard audit report\n"
report = report + "risk: \{self.risk_level()} (score=\{self.risk_score()})\n"
report = report + "action: \{self.recommended_action()}\n"
report = report +
"findings: dangerous=\{self.dangerous_count()}, warning=\{self.warning_count()}, info=\{self.info_count()}\n"
report = report + "safe text: \{self.text}\n"
if self.findings.length() == 0 {
report = report + "details: no terminal control sequences found\n"
} else {
report = report + "details:\n"
for finding in self.findings {
report = report +
"- \{finding.kind.label()} [\{finding.severity.label()}] offset \{finding.offset}: \{finding.message}; sequence=\{finding.sequence_preview()}\n"
}
}
report
}
///|
fn visible_sequence(sequence : String) -> String {
let mut output = ""
for char in sequence {
if char == '\u{1b}' {
output = output + "\\e"
} else if char == '\u{07}' {
output = output + "\\a"
} else if char == '\r' {
output = output + "\\r"
} else if char == '\n' {
output = output + "\\n"
} else if char == '\t' {
output = output + "\\t"
} else if char == '\b' {
output = output + "\\b"
} else if char.to_int() < 0x20 || char.to_int() == 0x7f {
output = output + ""
} else if char.to_int() >= 0x80 && char.to_int() <= 0x9f {
output = output + ""
} else {
output = output + "\{char}"
}
}
output
}
///|
fn append_range(
output : Array[Char],
chars : Array[Char],
start : Int,
end : Int,
) -> Unit {
for index = start; index < end; index = index + 1 {
output.push(chars[index])
}
}
///|
fn sequence_text(chars : Array[Char], start : Int, end : Int) -> String {
String::from_array(chars[start:end])
}
///|
fn add_finding(
findings : Array[Finding],
kind : FindingKind,
severity : Severity,
offset : Int,
sequence : String,
message : String,
) -> Unit {
findings.push({ kind, severity, offset, sequence, message })
}
///|
fn is_csi_final(char : Char) -> Bool {
let code = char.to_int()
code >= 0x40 && code <= 0x7e
}
///|
fn classify_mode_change(parameters : String) -> String {
if parameters.contains("?2004") {
"bracketed-paste mode change can alter how later input is interpreted"
} else if parameters.contains("?1000") ||
parameters.contains("?1002") ||
parameters.contains("?1003") ||
parameters.contains("?1005") ||
parameters.contains("?1006") ||
parameters.contains("?1015") {
"mouse-tracking mode change can cause the terminal to emit input reports"
} else if parameters.contains("?1004") {
"focus-event mode change can cause the terminal to emit focus reports"
} else if parameters.contains("?1047") ||
parameters.contains("?1048") ||
parameters.contains("?1049") {
"alternate-screen mode change can hide previously visible output"
} else {
"terminal mode change can affect later input or rendering behavior"
}
}
///|
fn classify_csi(
parameters : String,
final_char : Char,
) -> (FindingKind, Severity, String) {
let is_cursor = final_char == 'A' ||
final_char == 'B' ||
final_char == 'C' ||
final_char == 'D' ||
final_char == 'E' ||
final_char == 'F' ||
final_char == 'G' ||
final_char == 'H' ||
final_char == 'f' ||
final_char == 'd' ||
final_char == 's' ||
final_char == 'u'
let is_screen = final_char == 'J' ||
final_char == 'K' ||
final_char == 'S' ||
final_char == 'T' ||
final_char == 'L' ||
final_char == 'M' ||
final_char == '@' ||
final_char == 'P' ||
final_char == 'X' ||
final_char == 'r'
if final_char == 'm' {
(StyleSequence, Info, "SGR style sequence")
} else if final_char == 'h' || final_char == 'l' {
(ModeChange, Warning, classify_mode_change(parameters))
} else if final_char == 'n' || final_char == 'c' {
(
TerminalQuery,
Warning,
"terminal query can trigger a device response on the input channel",
)
} else if final_char == 't' {
(
WindowControl,
Warning,
"window operation can resize, move, minimize, or query the terminal",
)
} else if final_char == 'q' {
(ModeChange, Warning, "cursor or terminal presentation mode change")
} else if is_cursor {
(
CursorControl,
Warning,
"cursor movement can make logs visually misleading",
)
} else if is_screen {
(
ScreenControl,
Warning,
"screen editing can hide or rewrite visible output",
)
} else {
(DeviceControl, Warning, "terminal mode or device control sequence")
}
}
///|
fn classify_osc(
chars : Array[Char],
payload_start : Int,
payload_end : Int,
) -> (FindingKind, Severity, String) {
let mut separator = payload_start
while separator < payload_end && chars[separator] != ';' {
separator = separator + 1
}
let command = sequence_text(chars, payload_start, separator)
if command == "52" {
(
ClipboardWrite,
Dangerous,
"OSC 52 can write attacker-controlled data to the clipboard",
)
} else if command == "8" {
(
Hyperlink,
Warning,
"OSC 8 can display a link target different from visible text",
)
} else if command == "0" || command == "1" || command == "2" {
(TitleChange, Warning, "terminal title modification")
} else if command == "7" {
(
WorkingDirectory,
Warning,
"working-directory metadata can disclose or spoof a local path",
)
} else if command == "9" || command == "777" {
(
Notification,
Warning,
"desktop notification can display attacker-controlled content",
)
} else if command == "133" {
(
ShellIntegration,
Warning,
"shell-integration markers can spoof prompt and command boundaries",
)
} else if command == "1337" {
(
FileTransfer,
Dangerous,
"terminal-specific file or inline-image transfer payload",
)
} else if command == "4" ||
command == "10" ||
command == "11" ||
command == "12" ||
command == "17" ||
command == "19" {
(
PaletteChange,
Warning,
"terminal palette modification can make text misleading or invisible",
)
} else {
(DeviceControl, Warning, "operating-system command sequence")
}
}
///|
fn scan_csi(
chars : Array[Char],
start : Int,
prefix_length : Int,
policy : Policy,
output : Array[Char],
findings : Array[Finding],
) -> Int {
let length = chars.length()
let mut cursor = start + prefix_length
let limit = start + policy.max_sequence_length
while cursor < length && cursor < limit && !is_csi_final(chars[cursor]) {
cursor = cursor + 1
}
if cursor < length && cursor < limit {
let end = cursor + 1
let parameters = sequence_text(chars, start + prefix_length, cursor)
let (kind, severity, message) = classify_csi(parameters, chars[cursor])
let sequence = sequence_text(chars, start, end)
if kind == StyleSequence && policy.allow_sgr {
append_range(output, chars, start, end)
} else {
add_finding(findings, kind, severity, start, sequence, message)
}
end
} else if cursor >= limit {
let end = if limit < length { limit } else { length }
add_finding(
findings,
OversizedSequence,
Dangerous,
start,
sequence_text(chars, start, end),
"control sequence exceeded the configured length limit",
)
end
} else {
add_finding(
findings,
TruncatedSequence,
Dangerous,
start,
sequence_text(chars, start, length),
"unterminated CSI sequence",
)
length
}
}
///|
fn scan_string_control(
chars : Array[Char],
start : Int,
prefix_length : Int,
policy : Policy,
kind_name : String,
output : Array[Char],
findings : Array[Finding],
) -> Int {
let length = chars.length()
let payload_start = start + prefix_length
let limit = start + policy.max_sequence_length
let mut cursor = payload_start
let mut found_terminator = false
let mut terminator_length = 0
while cursor < length && cursor < limit && !found_terminator {
if kind_name == "OSC" && chars[cursor] == '\u{07}' {
found_terminator = true
terminator_length = 1
} else if chars[cursor].to_int() == 0x9c {
found_terminator = true
terminator_length = 1
} else if chars[cursor] == '\u{1b}' &&
cursor + 1 < length &&
chars[cursor + 1] == '\\' {
found_terminator = true
terminator_length = 2
} else {
cursor = cursor + 1
}
}
if found_terminator {
let end = cursor + terminator_length
let (kind, severity, message) = if kind_name == "OSC" {
classify_osc(chars, payload_start, cursor)
} else {
(
DeviceControl,
Dangerous,
"\{kind_name} payload can trigger terminal-specific behavior",
)
}
if kind == Hyperlink && policy.allow_hyperlinks {
append_range(output, chars, start, end)
} else {
add_finding(
findings,
kind,
severity,
start,
sequence_text(chars, start, end),
message,
)
}
end
} else if cursor >= limit {
let end = if limit < length { limit } else { length }
add_finding(
findings,
OversizedSequence,
Dangerous,
start,
sequence_text(chars, start, end),
"\{kind_name} sequence exceeded the configured length limit",
)
end
} else {
add_finding(
findings,
TruncatedSequence,
Dangerous,
start,
sequence_text(chars, start, length),
"unterminated \{kind_name} sequence",
)
length
}
}
///|
fn scan_escape(
chars : Array[Char],
start : Int,
policy : Policy,
output : Array[Char],
findings : Array[Finding],
) -> Int {
let length = chars.length()
if start + 1 >= length {
add_finding(
findings,
TruncatedSequence,
Dangerous,
start,
sequence_text(chars, start, length),
"trailing escape character",
)
return length
}
let introducer = chars[start + 1]
if introducer == '[' {
scan_csi(chars, start, 2, policy, output, findings)
} else if introducer == ']' {
scan_string_control(chars, start, 2, policy, "OSC", output, findings)
} else if introducer == 'P' {
scan_string_control(chars, start, 2, policy, "DCS", output, findings)
} else if introducer == '_' {
scan_string_control(chars, start, 2, policy, "APC", output, findings)
} else if introducer == '^' {
scan_string_control(chars, start, 2, policy, "PM", output, findings)
} else if introducer == '(' ||
introducer == ')' ||
introducer == '*' ||
introducer == '+' ||
introducer == '-' ||
introducer == '.' ||
introducer == '/' ||
introducer == '%' {
if start + 2 >= length {
add_finding(
findings,
TruncatedSequence,
Dangerous,
start,
sequence_text(chars, start, length),
"truncated character-set selection sequence",
)
length
} else {
let end = start + 3
add_finding(
findings,
CharacterSet,
Info,
start,
sequence_text(chars, start, end),
"terminal character-set selection",
)
end
}
} else if introducer == '#' {
if start + 2 >= length {
add_finding(
findings,
TruncatedSequence,
Dangerous,
start,
sequence_text(chars, start, length),
"truncated terminal test sequence",
)
length
} else {
let end = start + 3
add_finding(
findings,
ScreenControl,
Warning,
start,
sequence_text(chars, start, end),
"terminal screen-alignment or test sequence",
)
end
}
} else if introducer == '7' || introducer == '8' {
let end = start + 2
add_finding(
findings,
CursorControl,
Warning,
start,
sequence_text(chars, start, end),
"save or restore cursor state",
)
end
} else if introducer == 'D' ||
introducer == 'E' ||
introducer == 'M' ||
introducer == 'c' {
let end = start + 2
add_finding(
findings,
ScreenControl,
Warning,
start,
sequence_text(chars, start, end),
"terminal index, reverse-index, newline, or reset operation",
)
end
} else if introducer == '=' || introducer == '>' {
let end = start + 2
add_finding(
findings,
ModeChange,
Warning,
start,
sequence_text(chars, start, end),
"terminal keypad mode change",
)
end
} else {
let end = start + 2
add_finding(
findings,
UnknownEscape,
Warning,
start,
sequence_text(chars, start, end),
"unknown or two-byte escape sequence",
)
end
}
}
///|
fn scan_c1(
chars : Array[Char],
start : Int,
policy : Policy,
output : Array[Char],
findings : Array[Finding],
) -> Int {
let code = chars[start].to_int()
if code == 0x9b {
scan_csi(chars, start, 1, policy, output, findings)
} else if code == 0x9d {
scan_string_control(chars, start, 1, policy, "OSC", output, findings)
} else if code == 0x90 {
scan_string_control(chars, start, 1, policy, "DCS", output, findings)
} else if code == 0x9f {
scan_string_control(chars, start, 1, policy, "APC", output, findings)
} else if code == 0x9e {
scan_string_control(chars, start, 1, policy, "PM", output, findings)
} else {
add_finding(
findings,
ControlCharacter,
Warning,
start,
sequence_text(chars, start, start + 1),
"C1 control character",
)
start + 1
}
}
///|
/// Sanitizes terminal output and returns safe text plus an audit trail.
pub fn scan_with_policy(input : String, policy : Policy) -> ScanResult {
let chars = input.to_array()
let output : Array[Char] = []
let findings : Array[Finding] = []
let mut index = 0
while index < chars.length() {
let char = chars[index]
let code = char.to_int()
if char == '\u{1b}' {
index = scan_escape(chars, index, policy, output, findings)
} else if code >= 0x80 && code <= 0x9f {
index = scan_c1(chars, index, policy, output, findings)
} else if code < 0x20 || code == 0x7f {
if char == '\n' && policy.allow_newline {
output.push(char)
} else if char == '\t' && policy.allow_tab {
output.push(char)
} else {
let (severity, message) = if char == '\r' || char == '\b' {
(
Warning,
"carriage return or backspace can overwrite visible log text",
)
} else {
(Info, "C0 control character")
}
add_finding(
findings,
ControlCharacter,
severity,
index,
sequence_text(chars, index, index + 1),
message,
)
}
index = index + 1
} else {
output.push(char)
index = index + 1
}
}
{
text: String::from_array(output),
findings,
removed_count: findings.length(),
}
}
///|
/// Sanitizes terminal output with the strict default policy.
pub fn scan(input : String) -> ScanResult {
scan_with_policy(input, Policy::strict())
}
///|
/// Returns strict plain text without audit metadata.
pub fn sanitize(input : String) -> String {
scan(input).text()
}