ansi-guard

    Audit and sanitize untrusted ANSI terminal output in pure MoonBit

    terminal
    ansi
    security
    sanitizer
    logs
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    last month
    Downloads
    7

    #MoonAnsi Guard

    MoonAnsi Guard 是一个用 MoonBit 编写的终端控制序列安全清洗、流式解析与自动化审计库。

    当 AI Agent、CI 任务、远程命令或自动化测试系统产生不可信终端输出时,直接展示原始内容可能触发剪贴板写入、隐藏链接、标题修改、清屏、光标移动或日志改写。MoonAnsi Guard 的目标是把这些输出转换成安全文本,并给出结构化风险记录。

    项目不仅处理完整字符串,还能识别被故意拆分到多个数据块中的控制序列,并输出 JSON 报告、风险统计、报告脱敏结果以及 allowreviewblock 自动化门禁决策。

    #安装

    发布后可通过 Mooncakes 添加依赖:

    moon add SongYZZZ/ansi-guard

    在使用方包的 moon.pkg 中声明别名:

    import {
    "SongYZZZ/ansi-guard" @ansi_guard,
    }

    #不是普通“去颜色”

    普通 ANSI stripping 往往只删除颜色代码。MoonAnsi Guard 更关注终端输出的安全语义:

    输入类型默认处理风险
    CSI SGR 颜色/样式删除,可按策略保留
    光标移动、清屏、擦除删除并记录日志伪造
    OSC 8 超链接删除并记录显示文本与真实链接不一致
    OSC 0/2 标题修改删除并记录界面欺骗
    OSC 52 剪贴板写入删除并标记为危险修改用户剪贴板
    DCS、APC、PM删除并标记为危险终端特定行为
    回车、退格删除并记录覆盖可见日志
    未终止或超长序列截断并标记为危险解析绕过或资源消耗
    括号粘贴、鼠标、焦点、备用屏幕模式删除并记录改变后续输入或隐藏输出
    终端查询和窗口操作删除并记录触发终端响应或改变窗口状态
    工作目录、通知、Shell Integration删除并记录路径泄露、通知欺骗或提示符伪造
    iTerm2 文件/图片传输删除并标记为危险向终端传输攻击者控制的数据

    #快速开始

    let result = @ansi_guard.scan(
    "build: \u{1b}[31mFAILED\u{1b}[0m\u{1b}]52;c;SGVsbG8=\u{07}",
    )

    println(result.text())
    // build: FAILED

    println(result.risk_level())
    // high

    println(result.audit_report())

    流式扫描被拆分的控制序列:

    let scanner = @ansi_guard.StreamScanner::new(
    policy=@ansi_guard.Policy::agent(),
    )
    let session = @ansi_guard.AuditSession::new()

    session.record(scanner.feed("tool: \u{1b}]52;c;"))
    session.record(scanner.feed("secret"))
    session.record(scanner.feed("\u{07}done"))
    session.record(scanner.finish())

    let result = session.result()
    println(result.json_report_with(@ansi_guard.ReportPolicy::redacted()))

    只需要纯文本时:

    let safe = @ansi_guard.sanitize(untrusted_output)

    允许保留颜色和强调样式:

    let result = @ansi_guard.scan_with_policy(
    trusted_output,
    @ansi_guard.Policy::styled(),
    )

    #本地运行

    moon check moon build moon test moon run cmd/main

    直接审计一段终端输出:

    moon run cmd/audit -- --escaped "build ok\e]52;c;secret\a done"

    该命令会输出清洗后的文本、风险等级、风险分数、建议动作以及逐项命中记录。--escaped 便于在普通命令行中输入不可见控制字符;--styled 可保留 SGR 样式;--clean-only 只输出清洗后的文本。

    #演示内容

    主演示覆盖 8 类风险和 1 组策略对比:

    案例覆盖能力
    彩色构建日志CSI SGR 样式序列
    剪贴板写入OSC 52
    隐藏超链接OSC 8
    窗口标题伪装OSC 0/2
    光标与清屏改写CSI 光标移动、擦除
    回车与退格改写C0 控制字符
    私有终端负载DCS、APC、PM
    截断序列未终止控制序列
    策略对比strict/styled

    每个案例都会输出安全文本、移除数量、严重级别统计、风险等级、风险分数和审计记录。

    #示例工程

    moon run examples/ci-gate moon run examples/agent-output moon run examples/stream-audit moon run examples/policy-gate

    • examples/ci-gate:模拟 CI 日志安全门禁。
    • examples/agent-output:模拟 AI Agent 工具输出清洗。
    • examples/stream-audit:演示跨数据块剪贴板写入和隐藏链接审计。
    • examples/policy-gate:比较严格、均衡和宽松门禁策略。
    • examples/attack-cases.md:列出可用于演示和验收的攻击样例。

    #API

    • sanitize(String) -> String:使用严格策略返回安全纯文本。
    • scan(String) -> ScanResult:使用严格策略返回文本与审计记录。
    • scan_with_policy(String, Policy) -> ScanResult:使用自定义策略。
    • Policy::strict():删除所有终端控制效果,默认保留换行与制表符。
    • Policy::styled():额外保留 SGR 颜色/强调序列。
    • Policy::ci()Policy::agent()Policy::single_line_archive():场景化策略预设。
    • Policy::with_*():在现有策略基础上创建安全的修改副本。
    • Policy::new(...):创建自定义策略。
    • StreamScanner::new()feed()finish():跨分块增量扫描。
    • AuditSession::record()result():汇总多段扫描结果。
    • GatePolicy::strict()balanced()permissive()new():自动化门禁策略。
    • ReportPolicy::full()bounded()redacted()metadata_only():报告内容与脱敏策略。

    Finding 提供:

    • kind()severity()offset()sequence()sequence_preview()message()

    ScanResult 提供:

    • text()findings()removed_count()is_safe()
    • count_severity()dangerous_count()warning_count()info_count()
    • risk_score()risk_level()audit_report()
    • recommended_action():返回适合自动化接入的 allowreviewblock
    • recommended_decision()decide_with()decision_reasons():类型化决策与门禁原因。
    • count_kind()kind_counts()summary():风险类别统计。
    • json_report()json_report_with()redacted_audit_report():机器报告与脱敏报告。

    #文档

    #项目边界

    MoonAnsi Guard 是一个终端输出安全库,不是终端模拟器、Shell、PTY 工具、TUI 框架或日志平台。它只处理已经产生的字符串,不执行命令,也不判断命令本身是否恶意。

    这个边界让它更容易嵌入 CI、日志平台、聊天机器人、AI Agent 工具链和自动化测试系统。

    #开源与查重

    项目采用 Apache-2.0 许可证。代码为原创 MoonBit 实现,不包含移植代码。选题查重方法、相邻项目和差异说明见 docs/novelty-review.md

    #状态

    当前开发版本为 0.1.0。项目包含 17 个 MoonBit 源文件、91 项自动化测试、流式扫描器、协议分类器、策略系统、自动化门禁、JSON/脱敏报告、四个独立示例、README、CI、许可证、查重说明和提交材料。

    AuditSession

    pub struct AuditSession {
    segment_count : Int
    safe_text : String
    findings : Array[Finding]
    }

    Aggregates results from multiple chunks or independent scan operations.

    AuditSession::finding_count

    fn AuditSession::finding_count(self : AuditSession) -> Int

    AuditSession::new

    AuditSession::record

    fn AuditSession::record(self : AuditSession, result : ScanResult) -> Unit

    Adds one scan result while preserving its finding offsets.

    AuditSession::reset

    fn AuditSession::reset(self : AuditSession) -> Unit

    AuditSession::result

    fn AuditSession::result(self : AuditSession) -> ScanResult

    Returns an immutable snapshot compatible with the normal reporting API.

    AuditSession::segment_count

    fn AuditSession::segment_count(self : AuditSession) -> Int

    AuditSummary

    pub struct AuditSummary {
    risk_level : String
    risk_score : Int
    decision : Decision
    total_findings : Int
    dangerous : Int
    warning : Int
    info : Int
    kind_counts : Array[KindCount]
    } derive(Eq,
    Debug
    )

    Stable summary of an audit result.

    AuditSummary::dangerous_count

    fn AuditSummary::dangerous_count(self : AuditSummary) -> Int

    AuditSummary::decision

    fn AuditSummary::decision(self : AuditSummary) -> Decision

    AuditSummary::info_count

    fn AuditSummary::info_count(self : AuditSummary) -> Int

    AuditSummary::kind_counts

    fn AuditSummary::kind_counts(self : AuditSummary) -> Array[KindCount]

    AuditSummary::risk_level

    fn AuditSummary::risk_level(self : AuditSummary) -> String

    AuditSummary::risk_score

    fn AuditSummary::risk_score(self : AuditSummary) -> Int

    AuditSummary::total_findings

    fn AuditSummary::total_findings(self : AuditSummary) -> Int

    AuditSummary::warning_count

    fn AuditSummary::warning_count(self : AuditSummary) -> Int

    Decision

    pub(all) enum Decision {
    Allow
    Review
    Block
    } derive(Eq,
    Debug
    )

    Recommended handling decision for audited output.

    Decision::label

    fn Decision::label(self : Decision) -> String

    Returns a stable machine-friendly decision label.

    Finding

    pub struct Finding {
    kind : FindingKind
    severity : Severity
    offset : Int
    sequence : String
    message : String
    } derive(Eq,
    Debug
    )

    One item removed from or observed in terminal output.

    Finding::kind

    fn Finding::kind(self : Finding) -> FindingKind

    Finding::message

    fn Finding::message(self : Finding) -> String

    Finding::offset

    fn Finding::offset(self : Finding) -> Int

    Finding::sequence

    fn Finding::sequence(self : Finding) -> String

    Finding::sequence_preview

    fn Finding::sequence_preview(self : Finding) -> String

    Returns the captured sequence with common control bytes made visible.

    Finding::severity

    fn Finding::severity(self : Finding) -> Severity

    Finding::to_json

    fn Finding::to_json(self : Finding) -> String

    Renders one finding as a stable JSON object.

    Finding::to_json_with

    fn Finding::to_json_with(self : Finding, policy : ReportPolicy) -> String

    Renders one finding according to a report redaction policy.

    FindingKind

    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
    )

    Category of a terminal control sequence or control character.

    FindingKind::label

    fn FindingKind::label(self : FindingKind) -> String

    Returns a stable machine-friendly category name.

    GatePolicy

    pub struct GatePolicy {
    block_on_dangerous : Bool
    block_score : Int
    review_score : Int
    review_finding_count : Int
    blocked_kinds : Array[FindingKind]
    } derive(Eq,
    Debug
    )

    Configures how audit findings become allow, review, or block decisions.

    GatePolicy::balanced

    fn GatePolicy::balanced() -> GatePolicy

    Balanced default that reviews warnings and blocks dangerous output.

    GatePolicy::block_threshold

    fn GatePolicy::block_threshold(self : GatePolicy) -> Int

    GatePolicy::blocked_kinds

    fn GatePolicy::blocked_kinds(self : GatePolicy) -> Array[FindingKind]

    GatePolicy::blocks_dangerous

    fn GatePolicy::blocks_dangerous(self : GatePolicy) -> Bool

    GatePolicy::finding_threshold

    fn GatePolicy::finding_threshold(self : GatePolicy) -> Int

    GatePolicy::new

    fn GatePolicy::new(block_on_dangerous? : Bool, block_score? : Int, review_score? : Int, review_finding_count? : Int, blocked_kinds? : Array[FindingKind]) -> GatePolicy

    Creates a custom gate policy.

    GatePolicy::permissive

    fn GatePolicy::permissive() -> GatePolicy

    Policy for trusted internal terminals that still blocks critical payloads.

    GatePolicy::review_threshold

    fn GatePolicy::review_threshold(self : GatePolicy) -> Int

    GatePolicy::strict

    fn GatePolicy::strict() -> GatePolicy

    Conservative policy suitable for public CI output.

    KindCount

    pub struct KindCount {
    kind : FindingKind
    count : Int
    } derive(Eq,
    Debug
    )

    Count for one finding category.

    KindCount::count

    fn KindCount::count(self : KindCount) -> Int

    KindCount::kind

    fn KindCount::kind(self : KindCount) -> FindingKind

    Policy

    pub struct Policy {
    allow_sgr : Bool
    allow_hyperlinks : Bool
    allow_newline : Bool
    allow_tab : Bool
    max_sequence_length : Int
    } derive(Eq,
    Debug
    )

    Controls which harmless terminal features may pass through.

    Policy::agent

    fn Policy::agent() -> Policy

    Creates a policy tuned for untrusted AI Agent tool output.
    fn Policy::allows_hyperlinks(self : Policy) -> Bool

    Policy::allows_newline

    fn Policy::allows_newline(self : Policy) -> Bool

    Policy::allows_sgr

    fn Policy::allows_sgr(self : Policy) -> Bool

    Policy::allows_tab

    fn Policy::allows_tab(self : Policy) -> Bool

    Policy::ci

    fn Policy::ci() -> Policy

    Creates a policy tuned for CI logs.

    It removes presentation effects, preserves line structure, and uses a conservative sequence-size limit.

    Policy::description

    fn Policy::description(self : Policy) -> String

    Returns a stable short description suitable for diagnostics.

    Policy::new

    fn Policy::new(allow_sgr? : Bool, allow_hyperlinks? : Bool, allow_newline? : Bool, allow_tab? : Bool, max_sequence_length? : Int) -> Policy

    Creates a custom policy.

    Policy::sequence_limit

    fn Policy::sequence_limit(self : Policy) -> Int

    Policy::single_line_archive

    fn Policy::single_line_archive() -> Policy

    Creates a policy for canonical single-line archive records.

    Policy::strict

    fn Policy::strict() -> Policy

    Creates a strict policy suitable for untrusted logs and AI tool output.

    Policy::styled

    fn Policy::styled() -> Policy

    Creates a policy that keeps SGR color and emphasis sequences.
    fn Policy::with_hyperlinks(self : Policy, allowed : Bool) -> Policy

    Returns a copy with OSC 8 hyperlinks enabled or disabled.

    Policy::with_newline

    fn Policy::with_newline(self : Policy, allowed : Bool) -> Policy

    Returns a copy with newline preservation enabled or disabled.

    Policy::with_sequence_limit

    fn Policy::with_sequence_limit(self : Policy, limit : Int) -> Policy

    Returns a copy with a new maximum control-sequence length.

    Policy::with_sgr

    fn Policy::with_sgr(self : Policy, allowed : Bool) -> Policy

    Returns a copy with SGR styling enabled or disabled.

    Policy::with_tab

    fn Policy::with_tab(self : Policy, allowed : Bool) -> Policy

    Returns a copy with tab preservation enabled or disabled.

    ReportPolicy

    pub struct ReportPolicy {
    include_safe_text : Bool
    include_sequences : Bool
    safe_text_limit : Int
    sequence_limit : Int
    } derive(Eq,
    Debug
    )

    Controls how much potentially sensitive content is included in reports.

    ReportPolicy::bounded

    fn ReportPolicy::bounded() -> ReportPolicy

    Includes content but limits large fields to practical diagnostic sizes.

    ReportPolicy::full

    Includes complete sanitized text and captured control sequences.

    ReportPolicy::includes_safe_text

    fn ReportPolicy::includes_safe_text(self : ReportPolicy) -> Bool

    ReportPolicy::includes_sequences

    fn ReportPolicy::includes_sequences(self : ReportPolicy) -> Bool

    ReportPolicy::metadata_only

    fn ReportPolicy::metadata_only() -> ReportPolicy

    Emits only classification and aggregate metadata.

    ReportPolicy::new

    fn ReportPolicy::new(include_safe_text? : Bool, include_sequences? : Bool, safe_text_limit? : Int, sequence_limit? : Int) -> ReportPolicy

    ReportPolicy::redacted

    fn ReportPolicy::redacted() -> ReportPolicy

    Keeps sanitized text but removes captured terminal payloads.

    ReportPolicy::safe_text_max_length

    fn ReportPolicy::safe_text_max_length(self : ReportPolicy) -> Int

    ReportPolicy::sequence_max_length

    fn ReportPolicy::sequence_max_length(self : ReportPolicy) -> Int

    ScanResult

    pub struct ScanResult {
    text : String
    findings : Array[Finding]
    removed_count : Int
    } derive(Eq,
    Debug
    )

    Result of sanitizing one string.

    ScanResult::audit_report

    fn ScanResult::audit_report(self : ScanResult) -> String

    Renders a compact human-readable audit report.

    ScanResult::count_kind

    fn ScanResult::count_kind(self : ScanResult, kind : FindingKind) -> Int

    Counts findings with a specific category.

    ScanResult::count_severity

    fn ScanResult::count_severity(self : ScanResult, severity : Severity) -> Int

    ScanResult::dangerous_count

    fn ScanResult::dangerous_count(self : ScanResult) -> Int

    Counts dangerous findings.

    ScanResult::decide_with

    fn ScanResult::decide_with(self : ScanResult, policy : GatePolicy) -> Decision

    Applies a configurable automation gate.

    ScanResult::decision_reasons

    fn ScanResult::decision_reasons(self : ScanResult, policy : GatePolicy) -> Array[String]

    Explains which gate conditions were triggered.

    ScanResult::findings

    fn ScanResult::findings(self : ScanResult) -> Array[Finding]

    ScanResult::info_count

    fn ScanResult::info_count(self : ScanResult) -> Int

    Counts informational findings.

    ScanResult::is_safe

    fn ScanResult::is_safe(self : ScanResult) -> Bool

    ScanResult::json_report

    fn ScanResult::json_report(self : ScanResult) -> String

    Renders a machine-readable JSON audit report.

    ScanResult::json_report_with

    fn ScanResult::json_report_with(self : ScanResult, policy : ReportPolicy) -> String

    Renders a JSON report with configurable content redaction and limits.

    ScanResult::kind_counts

    fn ScanResult::kind_counts(self : ScanResult) -> Array[KindCount]

    Returns non-empty category counts in stable order.

    ScanResult::recommended_action

    fn ScanResult::recommended_action(self : ScanResult) -> String

    Returns a stable action label for automation and CI integration.

    ScanResult::recommended_decision

    fn ScanResult::recommended_decision(self : ScanResult) -> Decision

    Returns a typed handling decision.

    ScanResult::redacted_audit_report

    fn ScanResult::redacted_audit_report(self : ScanResult) -> String

    Renders a human-readable report without exposing captured sequence payloads.

    ScanResult::removed_count

    fn ScanResult::removed_count(self : ScanResult) -> Int

    ScanResult::risk_level

    fn ScanResult::risk_level(self : ScanResult) -> String

    Returns a stable coarse risk label.

    ScanResult::risk_score

    fn ScanResult::risk_score(self : ScanResult) -> Int

    Computes a small risk score for dashboards and CI gates.

    ScanResult::summary

    fn ScanResult::summary(self : ScanResult) -> AuditSummary

    Creates a compact structured summary.

    ScanResult::text

    fn ScanResult::text(self : ScanResult) -> String

    ScanResult::warning_count

    fn ScanResult::warning_count(self : ScanResult) -> Int

    Counts warning findings.

    Severity

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

    Severity assigned to a terminal-output finding.

    Severity::label

    fn Severity::label(self : Severity) -> String

    Returns a stable display label.

    StreamScanner

    pub struct StreamScanner {
    policy : Policy
    pending : String
    received_length : Int
    }

    Incrementally audits terminal output that arrives in multiple chunks.

    A scanner buffers only a trailing control sequence that may be completed by a later chunk. Plain text and complete sequences are emitted immediately.

    StreamScanner::feed

    fn StreamScanner::feed(self : StreamScanner, chunk : String) -> ScanResult

    Feeds one output chunk into the scanner.

    Finding offsets are relative to the complete logical stream, not to the current chunk.

    StreamScanner::finish

    fn StreamScanner::finish(self : StreamScanner) -> ScanResult

    Finalizes a logical stream.

    Any buffered partial sequence is audited as truncated. The scanner is reset after the result is produced and can be reused for another logical stream.

    StreamScanner::is_idle

    fn StreamScanner::is_idle(self : StreamScanner) -> Bool

    Returns true when no partial terminal sequence is buffered.

    StreamScanner::new

    fn StreamScanner::new(policy? : Policy) -> StreamScanner

    Creates a streaming scanner with the strict default policy.

    StreamScanner::pending_length

    fn StreamScanner::pending_length(self : StreamScanner) -> Int

    Returns the number of characters currently buffered as a partial sequence.

    StreamScanner::received_length

    fn StreamScanner::received_length(self : StreamScanner) -> Int

    Returns the total number of characters fed since creation or the last reset.

    StreamScanner::reset

    fn StreamScanner::reset(self : StreamScanner) -> Unit

    Discards buffered state without emitting a finding.

    sanitize

    fn sanitize(input : String) -> String

    Returns strict plain text without audit metadata.

    scan

    fn scan(input : String) -> ScanResult

    Sanitizes terminal output with the strict default policy.

    scan_with_policy

    fn scan_with_policy(input : String, policy : Policy) -> ScanResult

    Sanitizes terminal output and returns safe text plus an audit trail.