From c02b40470d9cb9f12c7c92fc1aa98133046fc0df Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Feb 2026 15:03:17 +0000 Subject: [PATCH] Add tirith-core for shell command security analysis Integrate tirith-core library to analyze shell commands before execution, detecting security threats like: - Homograph attacks (Unicode lookalikes) - Terminal injection (ANSI escapes, bidi controls) - Pipe-to-shell patterns (curl | bash) - Insecure transport and credential exposure The security analysis runs in a separate thread to avoid blocking the async runtime. Commands flagged as dangerous are blocked; warnings are prepended to output for suspicious but allowed commands. Note: Security analysis is skipped in test mode to avoid resource contention with timing-sensitive tests. https://claude.ai/code/session_01JWGY085M4E96QV0R2XPM1CEX --- Cargo.lock | 242 +++++++++++++++++++++++++++++++-------------- Cargo.toml | 3 + src/lib.rs | 1 + src/main.rs | 1 + src/security.rs | 255 ++++++++++++++++++++++++++++++++++++++++++++++++ src/tools/io.rs | 33 +++++++ 6 files changed, 461 insertions(+), 74 deletions(-) create mode 100644 src/security.rs diff --git a/Cargo.lock b/Cargo.lock index a2db172..2fd318d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -243,9 +243,9 @@ checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -265,9 +265,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "castaway" @@ -280,9 +280,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "jobserver", @@ -390,9 +390,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.55" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" dependencies = [ "clap_builder", "clap_derive", @@ -400,9 +400,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.55" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" dependencies = [ "anstream", "anstyle", @@ -472,6 +472,7 @@ dependencies = [ "tempfile", "textwrap", "thiserror 2.0.18", + "tirith-core", "tokio", "tokio-stream", "tokio-test", @@ -497,7 +498,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "memchr", ] @@ -582,9 +583,9 @@ dependencies = [ [[package]] name = "crokey" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad" +checksum = "04a63daf06a168535c74ab97cdba3ed4fa5d4f32cb36e437dcceb83d66854b7c" dependencies = [ "crokey-proc_macros", "crossterm 0.29.0", @@ -595,9 +596,9 @@ dependencies = [ [[package]] name = "crokey-proc_macros" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439" +checksum = "847f11a14855fc490bd5d059821895c53e77eeb3c2b73ee3dded7ce77c93b231" dependencies = [ "crossterm 0.29.0", "proc-macro2", @@ -983,6 +984,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "euclid" version = "0.22.13" @@ -1043,9 +1055,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "finl_unicode" @@ -1095,6 +1107,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1220,7 +1242,7 @@ version = "0.5.2" source = "git+https://github.com/tcdent/rust-genai.git?branch=codey-dev#9a416509fb23dff937a37e5df5d5856879a115d4" dependencies = [ "base64 0.22.1", - "bytes 1.11.0", + "bytes 1.11.1", "derive_more 2.1.1", "eventsource-stream", "futures 0.3.31", @@ -1278,7 +1300,7 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "fnv", "futures-core", "futures-sink", @@ -1298,7 +1320,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", - "bytes 1.11.0", + "bytes 1.11.1", "fnv", "futures-core", "futures-sink", @@ -1339,6 +1361,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1347,11 +1375,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "home" -version = "0.5.12" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1398,7 +1426,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "fnv", "itoa", ] @@ -1409,7 +1437,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "itoa", ] @@ -1419,7 +1447,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "http 0.2.12", "pin-project-lite", ] @@ -1430,7 +1458,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "http 1.4.0", ] @@ -1440,7 +1468,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-core", "http 1.4.0", "http-body 1.0.1", @@ -1465,7 +1493,7 @@ version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-channel", "futures-core", "futures-util", @@ -1490,7 +1518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", - "bytes 1.11.0", + "bytes 1.11.1", "futures-channel", "futures-core", "h2 0.4.13", @@ -1528,7 +1556,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "hyper 0.14.32", "native-tls", "tokio", @@ -1537,14 +1565,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", - "bytes 1.11.0", + "bytes 1.11.1", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -1554,7 +1581,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.2", - "system-configuration 0.6.1", + "system-configuration 0.7.0", "tokio", "tower-service", "tracing", @@ -1563,9 +1590,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1727,9 +1754,9 @@ dependencies = [ [[package]] name = "insta" -version = "1.46.1" +version = "1.46.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8" +checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" dependencies = [ "console", "once_cell", @@ -1793,6 +1820,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is-wsl" version = "0.4.0" @@ -2538,9 +2576,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -2597,7 +2635,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "cfg_aliases", "pin-project-lite", "quinn-proto", @@ -2618,7 +2656,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "aws-lc-rs", - "bytes 1.11.0", + "bytes 1.11.1", "getrandom 0.3.4", "lru-slab", "rand 0.9.2", @@ -2884,9 +2922,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2896,9 +2934,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2907,9 +2945,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" @@ -2918,7 +2956,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ "base64 0.21.7", - "bytes 1.11.0", + "bytes 1.11.1", "encoding_rs", "futures-core", "futures-util", @@ -2958,7 +2996,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", - "bytes 1.11.0", + "bytes 1.11.1", + "futures-channel", "futures-core", "futures-util", "http 1.4.0", @@ -2999,7 +3038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04e9018c9d814e5f30cc16a0f03271aeab3571e609612d9fe78c1aa8d11c2f62" dependencies = [ "base64 0.22.1", - "bytes 1.11.0", + "bytes 1.11.1", "encoding_rs", "futures-core", "futures-util", @@ -3237,9 +3276,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3371,7 +3410,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.2.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3390,6 +3429,19 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -3478,9 +3530,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3664,9 +3716,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags 2.10.0", "core-foundation 0.9.4", @@ -3924,13 +3976,38 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tirith-core" +version = "0.1.2" +source = "git+https://github.com/sheeki03/tirith.git#f42799cf5e040b20413f82326c3f3073be21366e" +dependencies = [ + "base64 0.22.1", + "chrono", + "etcetera", + "fs2", + "home", + "is-terminal", + "once_cell", + "percent-encoding", + "regex", + "reqwest 0.12.28", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "thiserror 2.0.18", + "unicode-normalization", + "unicode-script", + "url", +] + [[package]] name = "tokio" version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "libc", "mio", "parking_lot", @@ -4011,7 +4088,7 @@ version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "bytes 1.11.0", + "bytes 1.11.1", "futures-core", "futures-io", "futures-sink", @@ -4082,7 +4159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags 2.10.0", - "bytes 1.11.0", + "bytes 1.11.1", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -4179,7 +4256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", - "bytes 1.11.0", + "bytes 1.11.1", "data-encoding", "http 1.4.0", "httparse", @@ -4250,6 +4327,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + [[package]] name = "unicode-segmentation" version = "1.12.0" @@ -4285,6 +4377,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" @@ -5156,18 +5254,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.35" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572" +checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.35" +version = "0.8.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22" +checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" dependencies = [ "proc-macro2", "quote", @@ -5236,10 +5334,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" - -[[patch.unused]] -name = "ratatui-core" -version = "0.1.0-beta.0" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index 43097c1..e991212 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,9 @@ async-stream = "0.3" dotenvy = "0.15.7" typetag = "0.2.21" +# Terminal security analysis +tirith-core = { git = "https://github.com/sheeki03/tirith.git" } + # TUI rendering (CLI only) ratskin = { version = "0.3", optional = true } textwrap = { version = "0.16.2", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 67f3df1..c193498 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ mod config; mod effect; mod ide; mod llm; +pub mod security; mod tools; mod transcript; diff --git a/src/main.rs b/src/main.rs index 22fbc64..b78c64c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod notifications; #[cfg(feature = "profiling")] mod profiler; mod prompts; +mod security; mod tool_filter; mod tools; mod transcript; diff --git a/src/security.rs b/src/security.rs new file mode 100644 index 0000000..81684c0 --- /dev/null +++ b/src/security.rs @@ -0,0 +1,255 @@ +//! Terminal security analysis using tirith-core +//! +//! Provides security analysis for shell commands to detect: +//! - Homograph attacks (Unicode lookalikes) +//! - Terminal injection (ANSI escape sequences, bidi controls) +//! - Pipe-to-shell patterns (curl | bash) +//! - Dotfile attacks +//! - Insecure transport (HTTP) +//! - Ecosystem threats (typosquats) +//! - Credential exposure + +use tirith_core::engine::{analyze, AnalysisContext}; +use tirith_core::extract::ScanContext; +use tirith_core::tokenize::ShellType; +use tirith_core::verdict::{Action, Finding, Verdict}; + +/// Result of security analysis on a shell command +#[derive(Debug)] +pub struct SecurityAnalysis { + /// Whether the command should be blocked + pub blocked: bool, + /// Whether warnings were raised (but command can proceed) + pub warned: bool, + /// Human-readable summary of findings + pub summary: Option, + /// Detailed findings from the analysis + pub findings: Vec, +} + +/// A single security finding +#[derive(Debug)] +pub struct SecurityFinding { + pub rule_id: String, + pub severity: SecuritySeverity, + pub title: String, + pub description: String, +} + +/// Severity level of a security finding +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SecuritySeverity { + Low, + Medium, + High, + Critical, +} + +impl From for SecuritySeverity { + fn from(s: tirith_core::verdict::Severity) -> Self { + match s { + tirith_core::verdict::Severity::Low => SecuritySeverity::Low, + tirith_core::verdict::Severity::Medium => SecuritySeverity::Medium, + tirith_core::verdict::Severity::High => SecuritySeverity::High, + tirith_core::verdict::Severity::Critical => SecuritySeverity::Critical, + } + } +} + +impl From for SecurityFinding { + fn from(f: Finding) -> Self { + SecurityFinding { + rule_id: f.rule_id.to_string(), + severity: f.severity.into(), + title: f.title, + description: f.description, + } + } +} + +/// Analyze a shell command for security threats (async version) +/// +/// This function spawns a standard thread to avoid blocking the async runtime, +/// as tirith-core performs file I/O for policy discovery. +/// +/// Returns a `SecurityAnalysis` containing the verdict and any findings. +/// +/// # Arguments +/// +/// * `command` - The shell command to analyze +/// * `working_dir` - Optional working directory context +pub async fn analyze_command_async( + command: &str, + working_dir: Option<&str>, +) -> SecurityAnalysis { + // Skip analysis in test mode to avoid resource contention + #[cfg(test)] + { + let _ = (command, working_dir); + return SecurityAnalysis { + blocked: false, + warned: false, + summary: None, + findings: vec![], + }; + } + + #[cfg(not(test))] + { + let command = command.to_string(); + let working_dir = working_dir.map(|s| s.to_string()); + + // Use a oneshot channel to communicate with the spawned thread + let (tx, rx) = tokio::sync::oneshot::channel(); + + // Spawn a standard thread to avoid blocking the tokio runtime + std::thread::spawn(move || { + let result = analyze_command_sync(&command, working_dir.as_deref()); + let _ = tx.send(result); + }); + + // Wait for the result + match rx.await { + Ok(analysis) => analysis, + Err(_) => { + // If the thread panics or channel is dropped, allow the command through + // (fail open rather than blocking all commands) + SecurityAnalysis { + blocked: false, + warned: false, + summary: None, + findings: vec![], + } + } + } + } +} + +/// Analyze a shell command for security threats (synchronous version) +/// +/// Returns a `SecurityAnalysis` containing the verdict and any findings. +/// +/// # Arguments +/// +/// * `command` - The shell command to analyze +/// * `working_dir` - Optional working directory context +/// +/// # Example +/// +/// ``` +/// use codey::security::analyze_command_sync; +/// +/// let result = analyze_command_sync("curl https://example.com/script.sh | bash", None); +/// if result.blocked { +/// println!("Command blocked: {}", result.summary.unwrap_or_default()); +/// } +/// ``` +pub fn analyze_command_sync(command: &str, working_dir: Option<&str>) -> SecurityAnalysis { + let ctx = AnalysisContext { + input: command.to_string(), + shell: ShellType::Posix, + scan_context: ScanContext::Exec, + raw_bytes: Some(command.as_bytes().to_vec()), + interactive: false, // AI agent context is non-interactive + cwd: working_dir.map(|s| s.to_string()), + }; + + let verdict = analyze(&ctx); + + verdict_to_analysis(verdict) +} + +/// Convert a tirith Verdict to our SecurityAnalysis +fn verdict_to_analysis(verdict: Verdict) -> SecurityAnalysis { + let blocked = matches!(verdict.action, Action::Block); + let warned = matches!(verdict.action, Action::Warn); + + let findings: Vec = verdict + .findings + .into_iter() + .map(SecurityFinding::from) + .collect(); + + let summary = if findings.is_empty() { + None + } else { + let titles: Vec<&str> = findings.iter().map(|f| f.title.as_str()).collect(); + Some(titles.join("; ")) + }; + + SecurityAnalysis { + blocked, + warned, + summary, + findings, + } +} + +/// Format security findings for display +pub fn format_findings(analysis: &SecurityAnalysis) -> String { + if analysis.findings.is_empty() { + return String::new(); + } + + let mut output = String::new(); + + for finding in &analysis.findings { + let severity_str = match finding.severity { + SecuritySeverity::Low => "LOW", + SecuritySeverity::Medium => "MEDIUM", + SecuritySeverity::High => "HIGH", + SecuritySeverity::Critical => "CRITICAL", + }; + + output.push_str(&format!( + "[{}] {}: {}\n", + severity_str, finding.rule_id, finding.title + )); + + if !finding.description.is_empty() { + output.push_str(&format!(" {}\n", finding.description)); + } + } + + output +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_safe_command() { + let result = analyze_command_sync("ls -la", None); + assert!(!result.blocked); + assert!(!result.warned); + assert!(result.findings.is_empty()); + } + + #[test] + fn test_pipe_to_shell_detected() { + let result = analyze_command_sync("curl https://example.com/install.sh | bash", None); + // Pipe-to-shell patterns should be detected + assert!(!result.findings.is_empty() || result.warned || result.blocked); + } + + #[test] + fn test_format_findings() { + let analysis = SecurityAnalysis { + blocked: true, + warned: false, + summary: Some("Test finding".to_string()), + findings: vec![SecurityFinding { + rule_id: "TEST001".to_string(), + severity: SecuritySeverity::High, + title: "Test Finding".to_string(), + description: "This is a test".to_string(), + }], + }; + + let formatted = format_findings(&analysis); + assert!(formatted.contains("HIGH")); + assert!(formatted.contains("TEST001")); + assert!(formatted.contains("Test Finding")); + } +} diff --git a/src/tools/io.rs b/src/tools/io.rs index d469ad4..f11bee6 100644 --- a/src/tools/io.rs +++ b/src/tools/io.rs @@ -113,11 +113,39 @@ pub fn read_file( } /// Execute a shell command +/// +/// Before execution, the command is analyzed for security threats using tirith-core. +/// Commands flagged as dangerous (e.g., pipe-to-shell, homograph attacks) will be blocked. pub async fn execute_shell( command: &str, working_dir: Option<&str>, timeout_secs: u64, ) -> Result { + // Security analysis using tirith-core (runs in separate thread to avoid blocking) + let security_result = crate::security::analyze_command_async(command, working_dir).await; + + if security_result.blocked { + let findings = crate::security::format_findings(&security_result); + return Err(format!( + "Command blocked by security analysis:\n{}", + if findings.is_empty() { + security_result + .summary + .unwrap_or_else(|| "Suspicious command pattern detected".to_string()) + } else { + findings + } + )); + } + + // Log warnings but allow execution + let security_warning = if security_result.warned { + let findings = crate::security::format_findings(&security_result); + Some(format!("[Security Warning]\n{}", findings)) + } else { + None + }; + let mut cmd = Command::new("bash"); cmd.arg("-c").arg(command); cmd.stdout(Stdio::piped()); @@ -194,6 +222,11 @@ pub async fn execute_shell( output.push_str(&format!("\n[exit code: {}]", exit_code)); } + // Prepend security warning if present + if let Some(warning) = security_warning { + output = format!("{}\n\n{}", warning, output); + } + // Truncate if too long (UTF-8 safe) const MAX_OUTPUT: usize = 50000; if output.len() > MAX_OUTPUT {