termproof

Streaming ANSI/ECMA-48 terminal log sanitizer and security auditor for MoonBit

ansi
terminal
security
logs
ci
moon add DWS-ai-nb/termproof@0.1.0
Download zip
Author
Version
0.1.0
License
MIT
Last updated
14 hours ago
Downloads
3
README

#TermProof

TermProof is a MoonBit library and command-line tool for auditing untrusted terminal logs before they are shown in CI, issue trackers, release notes, or chat systems.

Terminal output is not plain text. ANSI/ECMA-48 control sequences can move the cursor, erase history, rewrite previous lines, set deceptive hyperlinks, write to the clipboard, or hide filenames with Unicode bidirectional controls. TermProof turns that output into deterministic text and a security report.

#Why This Project Is Different

Most terminal packages help programs produce nicer output. TermProof looks at the opposite side: it treats terminal output as hostile input. The project focuses on log integrity, reproducible auditing, and safe rendering instead of colors, TUIs, progress bars, or shell helpers.

#Features

  • Streaming ECMA-48 parser that handles split ESC, CSI, OSC, DCS, SOS, PM, and APC sequences.
  • Classification for cursor movement, screen erase, terminal queries, title changes, OSC 8 hyperlinks, OSC 52 clipboard writes, iTerm-style file transfer extensions, notifications, and device-control strings.
  • Unicode bidirectional-control detection for Trojan Source style filename or source-code disguise.
  • Four built-in policies: strict, ci, plain, and permissive.
  • Safe outputs: plain_text, safe_terminal, JSON, Markdown, and GitHub Actions annotations.
  • CLI commands for auditing, cleaning, event dumps, and a built-in demo.
  • Original fixtures and unit tests. No third-party log corpus is bundled.

#Installation

Install from mooncakes.io:

moon add DWS-ai-nb/termproof

Import the library from another package:

import {
"DWS-ai-nb/termproof" @termproof
}

#Quick Start

fn main {
let log = "ok\u{1b}[32mPASS\u{1b}[0m\n" +
"copy\u{1b}]52;c;SGVsbG8=\u{7}\n"
let result = @termproof.audit(log, policy=@termproof.Policy::strict())
println(@termproof.summary_line(result))
println(result.plain_text)
}

#CLI

Run the built-in example:

moon run cmd/termproof -- demo

Audit text and fail when the selected policy threshold is reached:

moon run cmd/termproof -- audit "trusted output" --policy strict

Write a Markdown report:

moon run cmd/termproof -- audit "trusted output" --format markdown

Clean text for display:

moon run cmd/termproof -- clean "trusted output" --policy plain

Emit parser events for debugging:

moon run cmd/termproof -- events "trusted output"

#Policies

PolicyIntended useKeeps SGR colorsKeeps OSC 8 linksMarks dangerous dataFails at
strictreview and artifact publishingnonoyesdangerous
ciCI logs that may keep colorsyesnonocritical
plainirreversible text exportnononocritical
permissivediagnostics and migrationyesyes, except high-risk schemesnocritical

#Supported Scope

TermProof is intentionally narrow. It parses terminal-control syntax and classifies security-relevant behavior in logs. It does not emulate a full terminal, execute shell commands, render a TUI, or validate every terminal-specific private extension. Unknown and malformed controls are still reported and removed according to policy.

#Development

moon check --deny-warn moon fmt --check moon build moon test --deny-warn moon run examples/audit moon run cmd/termproof -- audit "trusted output" --policy strict moon info moon package --list

The GitHub Actions workflow runs the same checks on every push and pull request. The default target is wasm-gc, so the project can build and test without requiring a local C compiler.

#Publishing

The module metadata in moon.mod is ready for mooncakes.io. Before publishing, confirm that dws-ai-nb matches the actual mooncakes.io owner, then run:

moon login moon package --list moon publish --frozen

Mooncakes uses the README and metadata from moon.mod for the package page, so both are kept as part of the release surface.

#License

TermProof is released under the MIT License. See LICENSE and THIRD_PARTY_NOTICES.md.

#
AuditResult

pub(all) struct AuditResult {
plain_text : String
safe_terminal : String
events : Array[Event]
findings : Array[Finding]
stats : Stats
failed : Bool
} derive(Eq,
Debug
)

Full result returned by audit.

#
CsiCommand

pub(all) struct CsiCommand {
private_prefix : String
params : Array[Int?]
intermediates : String
final_byte : Char
} derive(Eq,
Debug
)

Parsed view of a CSI payload. Missing parameters are represented as None; this preserves the semantic difference between an omitted value and zero.

#
Decision

pub(all) enum Decision {
Keep
Drop
Mark
} derive(Eq,
Debug
)

Action selected by a policy for an event.

#
Event

pub(all) struct Event {
kind : EventKind
raw : String
payload : String
final_byte : Char?
offset : Int
complete : Bool
} derive(Eq,
Debug
)

One event emitted by the streaming parser.

raw contains the original sequence, payload omits introducers and terminators, and final_byte is set for CSI/ESC commands that have one.

#
EventKind

pub(all) enum EventKind {
Text
C0
Escape
Csi
Osc
Dcs
Sos
Pm
Apc
C1
Invalid
Truncated
} derive(Eq,
Debug
)

Broad syntactic family of an ECMA-48 event.

#
Finding

pub(all) struct Finding {
rule_id : String
severity : Severity
risk : RiskKind
message : String
offset : Int
sequence : String
decision : Decision
} derive(Eq,
Debug
)

A human-readable security finding tied to an event offset.

#
OscCommand

pub(all) struct OscCommand {
command : String
data : String
} derive(Eq,
Debug
)

Parsed view of an OSC event.

#
Parser

pub(all) struct Parser {
mode : ParserMode
text : Array[Char]
sequence : Array[Char]
payload : Array[Char]
sequence_start : Int
offset : Int
}

Incremental ECMA-48 parser. It accepts arbitrary chunk boundaries, including a split between ESC and \\ in an ST terminator.

#
Parser::consumed

fn Parser::consumed(self : Parser) -> Int

#
Parser::current_mode

fn Parser::current_mode(self : Parser) -> ParserMode

#
Parser::feed

fn Parser::feed(self : Parser, chunk : String) -> Array[Event]

Feed a chunk and return all events completed by that chunk. Text is held until a control boundary or finish, so adjacent chunks coalesce naturally.

#
Parser::finish

fn Parser::finish(self : Parser) -> Array[Event]

Flush remaining text and turn an unfinished control string into a Truncated event. Calling finish resets the parser to Ground and is idempotent.

#
Parser::new

fn Parser::new() -> Parser

#
ParserMode

pub(all) enum ParserMode {
Ground
EscapeStart
EscapeIntermediate
CsiEntry
CsiParam
CsiIntermediate
OscString
OscEscape
DcsString
DcsEscape
SosString
SosEscape
PmString
PmEscape
ApcString
ApcEscape
} derive(Eq,
Debug
)

Parser mode that is exposed so callers can inspect streaming state.

#
Policy

pub(all) struct Policy {
name : String
keep_sgr : Bool
keep_hyperlinks : Bool
keep_bell : Bool
mark_dangerous : Bool
remove_bidi : Bool
fail_at : Severity
unknown_severity : Severity
max_sequence_chars : Int
} derive(Eq,
Debug
)

Sanitization and CI failure policy.

#
Policy::ci

fn Policy::ci() -> Policy

#
Policy::permissive

fn Policy::permissive() -> Policy

#
Policy::plain

fn Policy::plain() -> Policy

#
Policy::strict

fn Policy::strict() -> Policy

#
RiskKind

pub(all) enum RiskKind {
Styling
CursorMovement
ScreenRewrite
ScreenErase
TerminalQuery
ModeChange
TitleChange
Hyperlink
ClipboardWrite
Notification
WorkingDirectory
FileTransfer
DeviceControl
BidirectionalText
UnknownControl
MalformedSequence
} derive(Eq,
Debug
)

Security-relevant behavior inferred from a control sequence.

#
Severity

pub(all) enum Severity {
Info
Warning
Dangerous
Critical
} derive(Compare, Eq,
Debug
)

Severity assigned to a terminal-control finding.

#
Stats

pub(all) struct Stats {
input_chars : Int
text_chars : Int
control_events : Int
info : Int
warnings : Int
dangerous : Int
critical : Int
dropped : Int
marked : Int
} derive(Eq,
Debug
)

Summary counters for an audit run.

#
audit

fn audit(input : String, policy? : Policy) -> AuditResult

Parse, classify, and sanitize terminal output according to policy.

plain_text never contains terminal controls. safe_terminal may retain SGR or OSC 8 only when explicitly allowed by the selected policy.

#
classify

fn classify(event : Event, policy : Policy) -> Finding?

#
contains_char

fn contains_char(input : String, needle : Char) -> Bool

#
csi_param

fn csi_param(command : CsiCommand, index : Int, default : Int) -> Int

#
event_kind_name

fn event_kind_name(kind : EventKind) -> String

#
github_message_escape

fn github_message_escape(input : String) -> String

#
github_property_escape

fn github_property_escape(input : String) -> String

GitHub workflow command escaping as documented for annotation properties.

#
has_terminal_controls

fn has_terminal_controls(input : String) -> Bool

#
is_bidi_control

fn is_bidi_control(c : Char) -> Bool

#
is_suspicious_uri

fn is_suspicious_uri(uri : String) -> Bool

#
json_quote

fn json_quote(input : String) -> String

#
lower_ascii

fn lower_ascii(input : String) -> String

#
markdown_escape

fn markdown_escape(input : String) -> String

#
parse

fn parse(input : String) -> Array[Event]

Convenience batch parser.

#
parse_csi

fn parse_csi(event : Event) -> CsiCommand?

#
parse_osc

fn parse_osc(event : Event) -> OscCommand?

#
policy_named

fn policy_named(name : String) -> Policy?

#
report_github_annotations

fn report_github_annotations(result : AuditResult, file? : String) -> String

#
report_json

fn report_json(result : AuditResult, include_events? : Bool) -> String

Deterministic JSON report with no dependency on a JSON serialization package.

#
report_markdown

fn report_markdown(result : AuditResult) -> String

#
risk_name

fn risk_name(risk : RiskKind) -> String

#
sanitize

fn sanitize(input : String) -> String

#
sanitize_for_ci

fn sanitize_for_ci(input : String) -> String

#
severity_name

fn severity_name(severity : Severity) -> String

#
severity_rank

fn severity_rank(severity : Severity) -> Int

#
starts_with_ascii_case_insensitive

fn starts_with_ascii_case_insensitive(input : String, prefix : String) -> Bool

#
strip_bidi_controls

fn strip_bidi_controls(input : String) -> String

#
summary_line

fn summary_line(result : AuditResult) -> String

#
visible

fn visible(input : String) -> String

Render non-printing characters as stable ASCII. The result is safe to paste into terminals and useful in audit reports.

Powered by MoonBit

Site sourceReport issuePackagesBuild queueSkillsStatistics

© 2026 mooncakes.io