///|
pub(all) struct RuleCatalogEntry {
rule_id : String
risk : RiskKind
default_severity : Severity
category : String
title : String
explanation : String
safe_default : String
} derive(Debug, Eq)
///|
fn catalog_entry(
rule_id : String,
risk : RiskKind,
severity : Severity,
category : String,
title : String,
explanation : String,
safe_default : String,
) -> RuleCatalogEntry {
{
rule_id,
risk,
default_severity: severity,
category,
title,
explanation,
safe_default,
}
}
///|
pub fn rule_catalog() -> Array[RuleCatalogEntry] {
[
catalog_entry(
"TP-C0-001",
Notification,
Warning,
"C0 control",
"Bell notification",
"BEL can trigger audible or visual attention signals when replayed in terminals.",
"Drop unless the output is trusted and interactive.",
),
catalog_entry(
"TP-C0-002",
ScreenRewrite,
Dangerous,
"C0 control",
"Inline screen rewrite",
"Backspace and carriage return can visually overwrite earlier log text.",
"Mark or drop in public artifacts.",
),
catalog_entry(
"TP-C0-003",
UnknownControl,
Warning,
"C0 control",
"NUL in text stream",
"NUL is not meaningful in line-oriented text logs and can confuse consumers.",
"Drop from display output and keep raw input separately.",
),
catalog_entry(
"TP-C0-099",
UnknownControl,
Warning,
"C0 control",
"Unknown non-printing control",
"Non-printing C0 or C1 controls have terminal-specific behavior.",
"Drop unless an explicit allow-list documents the behavior.",
),
catalog_entry(
"TP-CSI-000",
MalformedSequence,
Dangerous,
"CSI",
"Malformed CSI sequence",
"A CSI command without a final byte is incomplete and unsafe to replay.",
"Drop and inspect the raw source around the offset.",
),
catalog_entry(
"TP-CSI-001",
Styling,
Info,
"CSI",
"SGR styling",
"Text style and color controls can reduce reproducibility or hide text in some themes.",
"Keep only for trusted CI display; strip from durable artifacts.",
),
catalog_entry(
"TP-CSI-010",
CursorMovement,
Dangerous,
"CSI",
"Cursor movement",
"Cursor movement can reorder visible output or overwrite earlier lines.",
"Reject in public logs unless captured from a trusted progress renderer.",
),
catalog_entry(
"TP-CSI-011",
ScreenErase,
Dangerous,
"CSI",
"Screen erase",
"Erase commands can hide previous output, and erase-scrollback variants are critical.",
"Block erase-scrollback; sanitize all erase commands before display.",
),
catalog_entry(
"TP-CSI-012",
ModeChange,
Dangerous,
"CSI",
"Terminal mode change",
"Mode changes can alter cursor, paste, alternate-screen, or keyboard behavior.",
"Drop unless a narrow trusted integration requires it.",
),
catalog_entry(
"TP-CSI-013",
TerminalQuery,
Warning,
"CSI",
"Terminal query",
"Device queries can solicit terminal responses and should not be replayed from logs.",
"Drop from archived logs and web output.",
),
catalog_entry(
"TP-CSI-014",
ScreenRewrite,
Dangerous,
"CSI",
"Saved cursor or scrolling region",
"Saved cursor, scroll, and region commands can make the visible log diverge from raw order.",
"Mark or drop and prefer non-interactive capture modes.",
),
catalog_entry(
"TP-CSI-015",
ModeChange,
Dangerous,
"CSI",
"Window manipulation",
"Window manipulation commands are unsafe in untrusted terminal transcripts.",
"Drop and review the producing command.",
),
catalog_entry(
"TP-CSI-099",
UnknownControl,
Warning,
"CSI",
"Unknown CSI command",
"Unknown CSI commands are not portable and may be interpreted differently by terminals.",
"Drop until a rule documents the exact behavior.",
),
catalog_entry(
"TP-OSC-000",
MalformedSequence,
Dangerous,
"OSC",
"Malformed OSC command",
"Malformed operating-system commands may hide payloads in terminal strings.",
"Drop and keep the visible context for review.",
),
catalog_entry(
"TP-OSC-001",
TitleChange,
Dangerous,
"OSC",
"Title change",
"Window or tab title updates can disguise the identity of a terminal session.",
"Drop from copied logs and generated documentation.",
),
catalog_entry(
"TP-OSC-004",
ModeChange,
Dangerous,
"OSC",
"Palette mutation",
"Palette changes can make later text misleading, invisible, or theme dependent.",
"Drop and publish theme-independent text.",
),
catalog_entry(
"TP-OSC-007",
WorkingDirectory,
Warning,
"OSC",
"Working-directory metadata",
"Directory metadata can leak local paths from personal machines or build agents.",
"Drop unless path disclosure is intentional.",
),
catalog_entry(
"TP-OSC-008",
Hyperlink,
Dangerous,
"OSC",
"Terminal hyperlink",
"OSC 8 can bind visible text to a hidden destination, including high-risk URI schemes.",
"Expand to visible URLs and block dangerous schemes.",
),
catalog_entry(
"TP-OSC-009",
Notification,
Dangerous,
"OSC",
"Desktop notification",
"Terminal notification extensions can create deceptive user-interface signals.",
"Drop outside trusted interactive sessions.",
),
catalog_entry(
"TP-OSC-052",
ClipboardWrite,
Critical,
"OSC",
"Clipboard write",
"OSC 52 can replace the user's clipboard when replayed by compatible terminals.",
"Block and regenerate the log from a trusted source.",
),
catalog_entry(
"TP-OSC-099",
UnknownControl,
Warning,
"OSC",
"Unknown OSC command",
"Unknown operating-system commands may trigger terminal-specific side effects.",
"Drop until reviewed and documented.",
),
catalog_entry(
"TP-OSC-133",
ModeChange,
Warning,
"OSC",
"Shell integration marker",
"Shell integration markers are metadata and should not affect archived log meaning.",
"Drop from untrusted output.",
),
catalog_entry(
"TP-OSC-1337",
FileTransfer,
Critical,
"OSC",
"Terminal file transfer",
"Terminal-specific extensions can embed file content or request file movement.",
"Block and store attachments as explicit reviewed files.",
),
catalog_entry(
"TP-STR-001",
DeviceControl,
Critical,
"Control string",
"Opaque device-control string",
"DCS, SOS, PM, and APC strings can carry device-specific opaque commands.",
"Block unless a documented allow-list covers the exact extension.",
),
catalog_entry(
"TP-ESC-001",
CursorMovement,
Dangerous,
"ESC",
"Escape cursor command",
"Single-byte escape cursor commands can rewrite the visible terminal surface.",
"Drop or mark in permanent logs.",
),
catalog_entry(
"TP-ESC-002",
ModeChange,
Critical,
"ESC",
"Full terminal reset",
"A full terminal reset changes terminal state and is unsafe to replay from artifacts.",
"Block release artifacts containing this control.",
),
catalog_entry(
"TP-ESC-099",
UnknownControl,
Warning,
"ESC",
"Unknown escape command",
"Unknown escape commands are not safe to replay across terminal emulators.",
"Drop unless explicitly reviewed.",
),
catalog_entry(
"TP-PARSE-001",
MalformedSequence,
Dangerous,
"Parser",
"Invalid or truncated sequence",
"Parser-level invalid data can indicate corrupted or evasive terminal input.",
"Drop and inspect the raw bytes around the offset.",
),
catalog_entry(
"TP-UNI-001",
BidirectionalText,
Dangerous,
"Unicode",
"Bidirectional text control",
"Unicode bidi controls can disguise visual order in filenames or source code.",
"Strip and review the surrounding text.",
),
]
}
///|
pub fn rule_catalog_find(rule_id : String) -> RuleCatalogEntry? {
for entry in rule_catalog() {
if entry.rule_id == rule_id {
return Some(entry)
}
}
None
}
///|
pub fn rule_catalog_for_risk(risk : RiskKind) -> Array[RuleCatalogEntry] {
let entries : Array[RuleCatalogEntry] = []
for entry in rule_catalog() {
if entry.risk == risk {
entries.push(entry)
}
}
entries
}
///|
pub fn rule_catalog_for_category(category : String) -> Array[RuleCatalogEntry] {
let entries : Array[RuleCatalogEntry] = []
for entry in rule_catalog() {
if lower_ascii(entry.category) == lower_ascii(category) {
entries.push(entry)
}
}
entries
}
///|
pub fn finding_catalog_entry(finding : Finding) -> RuleCatalogEntry? {
rule_catalog_find(finding.rule_id)
}
///|
pub fn rule_catalog_title(rule_id : String) -> String {
match rule_catalog_find(rule_id) {
Some(entry) => entry.title
None => "Unknown TermProof rule"
}
}
///|
pub fn rule_catalog_safe_default(rule_id : String) -> String {
match rule_catalog_find(rule_id) {
Some(entry) => entry.safe_default
None => "Drop and review the terminal control before display."
}
}
///|
pub fn rule_catalog_text() -> String {
let out = StringBuilder::new()
for entry in rule_catalog() {
out.write_string(entry.rule_id)
out.write_string(" ")
out.write_string(severity_name(entry.default_severity))
out.write_string(" ")
out.write_string(risk_name(entry.risk))
out.write_string(": ")
out.write_string(entry.title)
out.write_char('\n')
}
out.to_string()
}
///|
pub fn rule_catalog_markdown() -> String {
let out = StringBuilder::new()
out.write_string(
"| Rule | Severity | Risk | Category | Title | Safe default |\n",
)
out.write_string("|---|---|---|---|---|---|\n")
for entry in rule_catalog() {
out.write_string("| ")
out.write_string(markdown_escape(entry.rule_id))
out.write_string(" | ")
out.write_string(markdown_escape(severity_name(entry.default_severity)))
out.write_string(" | ")
out.write_string(markdown_escape(risk_name(entry.risk)))
out.write_string(" | ")
out.write_string(markdown_escape(entry.category))
out.write_string(" | ")
out.write_string(markdown_escape(entry.title))
out.write_string(" | ")
out.write_string(markdown_escape(entry.safe_default))
out.write_string(" |\n")
}
out.to_string()
}
///|
fn write_catalog_entry_json(
out : StringBuilder,
entry : RuleCatalogEntry,
) -> Unit {
out.write_string("{\"rule_id\":")
out.write_string(json_quote(entry.rule_id))
out.write_string(",\"severity\":")
out.write_string(json_quote(severity_name(entry.default_severity)))
out.write_string(",\"risk\":")
out.write_string(json_quote(risk_name(entry.risk)))
out.write_string(",\"category\":")
out.write_string(json_quote(entry.category))
out.write_string(",\"title\":")
out.write_string(json_quote(entry.title))
out.write_string(",\"explanation\":")
out.write_string(json_quote(entry.explanation))
out.write_string(",\"safe_default\":")
out.write_string(json_quote(entry.safe_default))
out.write_char('}')
}
///|
pub fn rule_catalog_json() -> String {
let out = StringBuilder::new()
out.write_string("{\"schema\":\"urn:termproof:rule-catalog-v1\",\"rules\":[")
let mut first = true
for entry in rule_catalog() {
if !first {
out.write_char(',')
}
first = false
write_catalog_entry_json(out, entry)
}
out.write_string("]}")
out.to_string()
}
///|
pub fn rule_catalog_count() -> Int {
rule_catalog().length()
}