diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..d8cdc04 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,734 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +dependencies = [ + "backtrace", +] + +[[package]] +name = "assert_cmd" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6" +dependencies = [ + "anstream", + "anstyle", + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0827b011f6f8ab38590295339817b0d26f344aa4932c3ced71b45b0c54b4a9" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441b403be87be858db6a23edb493e7f694761acdc3343d5a0fcaafd304cbc9e" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "static_assertions", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "predicates" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9" +dependencies = [ + "anstyle", + "difflib", + "itertools 0.10.5", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" + +[[package]] +name = "predicates-tree" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "serde" +version = "1.0.185" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" + +[[package]] +name = "shh" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "clap", + "fastrand", + "itertools 0.11.0", + "lazy_static", + "log", + "nix", + "predicates", + "pretty_assertions", + "regex", + "signal-hook", + "simple_logger", + "tempfile", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_logger" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230cd5c29b815c9b699fb610b49a5ed65588f3509d9f0108be3a885da629333" +dependencies = [ + "colored", + "log", + "windows-sys 0.42.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..582997d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "shh" +version = "0.1.0" +authors = ["Maxime Desbrus "] +description = "Automatic systemd service hardening guided by strace profiling" +readme = "README.md" +edition = "2021" + +[profile.release] +lto = true +codegen-units = 1 +strip = true + +[dependencies] +anyhow = { version = "1.0.72", default-features = false, features = ["std", "backtrace"] } +clap = { version = "4.3.17", default-features = false, features = ["std", "color", "help", "usage", "error-context", "suggestions", "derive"] } +itertools = { version = "0.11.0", default-features = false, features = ["use_std"] } +lazy_static = { version = "1.4.0", default-features = false } +log = { version = "0.4.19", default-features = false, features = ["max_level_trace", "release_max_level_info"] } +nix = { version = "0.26.2", default-features = false, features = ["fs"] } +regex = { version = "1.9.1", default-features = false, features = ["std", "perf"] } +signal-hook = { version = "0.3.17", default-features = false, features = ["iterator"] } +simple_logger = { version = "4.2.0", default-features = false, features = ["colors", "stderr"] } +tempfile = { version = "3.7.0", default-features = false } + +[dev-dependencies] +assert_cmd = { version = "2.0.12", default-features = false, features = ["color", "color-auto"] } +fastrand = { version = "2.0.0", default-features = false, features = ["std"] } +predicates = { version = "3.0.3", default-features = false, features = ["color"] } +pretty_assertions = { version = "1.4.0", default-features = false, features = ["std"] } + +[features] +nightly = [] + +[package.metadata.deb] +name = "shh" +depends = "$auto, strace" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dee405e --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# SHH (Systemd Hardening Helper) + +Automatic [systemd](https://systemd.io/) service hardening guided by [strace](https://strace.io/) profiling. + +## Installation from source + +You need a Rust build environment for example from [rustup](https://rustup.rs/). + +``` +cargo build --release +install -Dm 755 -t /usr/local/bin target/release/shh +``` + +## Usage + +To harden a system unit named `SERVICE.service`: + +1. Start service profiling: `shh service start-profile SERVICE`. The service will be restarted with strace profiling. +2. Use the service normally for a while, trying to cover as much features and use cases as possible. +3. Run `shh service finish-profile SERVICE -a`. The service will be restarted with a hardened configuration built from previous runtime profiling, to allow it to run safely as was observed during the profiling period, and to deny other dangerous system actions. + +Run `shh -h` for full command line reference, or append `-h` to a subcommand to get help. + +Services running in per-user instances of the service manager (controlled via `systemctl --user ...`) are **not** supported. + +## License + +[GPLv3](https://www.gnu.org/licenses/gpl-3.0-standalone.html) diff --git a/src/cl.rs b/src/cl.rs new file mode 100644 index 0000000..1a0a995 --- /dev/null +++ b/src/cl.rs @@ -0,0 +1,49 @@ +//! Command line interface + +use clap::Parser; + +#[derive(Parser, Debug)] +pub struct Args { + #[command(subcommand)] + pub action: Action, +} + +#[derive(Debug, clap::Subcommand)] +pub enum Action { + /// Run a program to profile its behavior + Run { + /// The command line to run + command: Vec, + }, + /// Act on a systemd service unit + #[clap(subcommand)] + Service(ServiceAction), +} + +#[derive(Debug, clap::Subcommand)] +pub enum ServiceAction { + /// Add fragment config to service to profile its behavior + StartProfile { + /// Service unit name + service: String, + /// Disable immediate service restart + #[arg(short, long, default_value_t = false)] + no_restart: bool, + }, + /// Get profiling result and remove fragment config from service + FinishProfile { + /// Service unit name + service: String, + /// Automatically apply hardening config + #[arg(short, long, default_value_t = false)] + apply: bool, + /// Disable immediate service restart + #[arg(short, long, default_value_t = false)] + no_restart: bool, + }, + /// Remove profiling and/or hardening config fragments, and restart service to restore its initial state + Reset { + /// Service unit name + service: String, + }, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..456d38b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,116 @@ +#![cfg_attr(all(feature = "nightly", test), feature(test))] + +use std::thread; + +use anyhow::Context; +use clap::Parser; + +mod cl; +mod strace; +mod summarize; +mod systemd; + +fn main() -> anyhow::Result<()> { + // Init logger + simple_logger::SimpleLogger::new() + .init() + .context("Failed to init logger")?; + + // Parse cl args + let args = cl::Args::parse(); + + // Get versions + let sd_version = systemd::SystemdVersion::local_system()?; + let kernel_version = systemd::KernelVersion::local_system()?; + log::info!("Detected Systemd version: {sd_version}, Linux kernel version: {kernel_version}"); + + // Build supported systemd options + let sd_opts = systemd::build_options(&sd_version, &kernel_version); + log::info!( + "Supported systemd options: {}", + sd_opts + .iter() + .map(|o| o.to_string()) + .collect::>() + .join(", ") + ); + + // Handle CL args + match args.action { + cl::Action::Run { command } => { + // Run strace + let cmd = command.iter().map(|a| &**a).collect::>(); + let st = strace::Strace::run(&cmd)?; + + // Start signal handling thread + let mut signals = signal_hook::iterator::Signals::new([ + signal_hook::consts::signal::SIGINT, + signal_hook::consts::signal::SIGTERM, + ])?; + thread::spawn(move || { + for sig in signals.forever() { + // The strace, and its watched child processes already get the signal, so the iterator will stop naturally + log::info!("Got signal {sig:?}, ignoring"); + } + }); + + // Summarize actions + let logs = st.log_lines()?; + let actions = summarize::summarize(logs)?; + log::debug!("{actions:?}"); + + // Resolve + let resolved_opts = systemd::resolve(&sd_opts, &actions)?; + + // Report + systemd::report_options(resolved_opts); + } + cl::Action::Service(cl::ServiceAction::StartProfile { + service, + no_restart, + }) => { + let service = systemd::Service::new(&service); + service.add_profile_fragment()?; + if !no_restart { + service.reload_unit_config()?; + service.action("restart")?; + } else { + log::warn!("Profiling config will only be applied when systemd config is reloaded, and service restarted"); + } + } + cl::Action::Service(cl::ServiceAction::FinishProfile { + service, + apply, + no_restart, + }) => { + let service = systemd::Service::new(&service); + service.action("stop")?; + service.remove_profile_fragment()?; + let resolved_opts = service.profiling_result()?; + log::info!( + "Resolved systemd options: {}", + resolved_opts + .iter() + .map(|o| format!("{}", o)) + .collect::>() + .join(", ") + ); + if apply && !resolved_opts.is_empty() { + service.add_hardening_fragment(resolved_opts)?; + } + service.reload_unit_config()?; + if !no_restart { + service.action("start")?; + } + } + cl::Action::Service(cl::ServiceAction::Reset { service }) => { + let service = systemd::Service::new(&service); + let _ = service.remove_profile_fragment(); + let _ = service.remove_hardening_fragment(); + service.reload_unit_config()?; + service.action("try-restart")?; + } + } + + Ok(()) +} diff --git a/src/strace/mod.rs b/src/strace/mod.rs new file mode 100644 index 0000000..b4f4c12 --- /dev/null +++ b/src/strace/mod.rs @@ -0,0 +1,76 @@ +//! Strace related code + +use std::collections::HashMap; + +mod parser; +mod run; + +pub use run::Strace; + +#[derive(Debug, Clone, PartialEq)] +pub struct Syscall { + pub pid: u32, + pub rel_ts: f64, + pub name: String, + pub args: Vec, + pub ret_val: SyscallRetVal, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum BufferType { + AbstractPath, + Unknown, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum SyscallArg { + Buffer { + value: Vec, + type_: BufferType, + }, + Integer { + value: IntegerExpression, + metadata: Option>, + }, + Struct(HashMap), + Array(Vec), + Macro { + name: String, + args: Vec, + }, +} + +impl SyscallArg { + pub fn metadata(&self) -> Option<&[u8]> { + match self { + Self::Integer { metadata, .. } => metadata.as_deref(), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum IntegerExpression { + BinaryNot(Box), + BinaryOr(Vec), + Multiplication(Vec), + LeftBitShift { + bits: Box, + shift: Box, + }, + NamedConst(String), + Literal(i128), // allows holding both signed and unsigned 64 bit integers +} + +impl IntegerExpression { + pub fn is_flag_set(&self, flag: &str) -> bool { + match self { + IntegerExpression::NamedConst(v) => flag == v, + IntegerExpression::BinaryOr(ces) => ces.iter().any(|ce| ce.is_flag_set(flag)), + IntegerExpression::BinaryNot(ce) => !ce.is_flag_set(flag), + _ => false, // if it was a flag field, strace would have decoded it with named consts + } + } +} + +pub type SyscallRetVal = i128; // allows holding both signed and unsigned 64 bit integers diff --git a/src/strace/parser.rs b/src/strace/parser.rs new file mode 100644 index 0000000..0f40975 --- /dev/null +++ b/src/strace/parser.rs @@ -0,0 +1,1679 @@ +//! Strace output parser + +use std::collections::HashMap; +use std::io::BufRead; +use std::str; + +use lazy_static::lazy_static; + +use crate::strace::{BufferType, IntegerExpression, Syscall, SyscallArg, SyscallRetVal}; + +pub struct LogParser { + reader: Box, + buf: String, + unfinished_syscalls: Vec, +} + +impl LogParser { + pub fn new(reader: Box) -> anyhow::Result { + Ok(Self { + reader, + buf: String::new(), + unfinished_syscalls: Vec::new(), + }) + } +} + +#[derive(Debug, PartialEq)] +enum ParseResult { + /// This line was ignored + /// (strace sometimes outputs complete garbage like '1008333 0.000045 ???( ') + IgnoredLine, + /// This line describes an unfinished syscall + UnfinishedSyscall(Syscall), + /// This line describes a previously unfinished syscall that is now finished + FinishedSyscall { + sc: Syscall, + unfinished_index: usize, + }, + /// This line describes a complete syscall + Syscall(Syscall), +} + +// See also: +// - https://github.com/rbtcollins/strace-parse.rs/blob/master/src/lib.rs for a nom based parsing approach +// - https://github.com/wookietreiber/strace-analyzer/blob/master/src/analysis.rs for a "1 regex per syscall" approach + +lazy_static! { + static ref LINE_REGEX: regex::Regex = regex::RegexBuilder::new( + r" +^ +(?[0-9]+)\ + +(?[0-9]+\.[0-9]+)\ + +( + ( + (?[a-z0-9_]+) + \( + (?.+)? + ) + | + ( + <\.{3}\ + (?[a-z0-9_]+) + \ resumed>\ + ) +) +( + ( + + \) + \ +=\ + ( + ( + 0x + (?[a-f0-9]+) + ) + | + ( + (?[-0-9]+) + ( + < + (?[^>]+) + > + ( + # (deleted) + \( + [^\)]+ + \) + )? + )? + ) + ) + ( + (\ E[A-Z]+\ \(.*\)) # errno + | + (\ \(.*\)) # interpretation like 'Timeout' + )? + ) + | + (?\ ) +) +$ +" + ) + .ignore_whitespace(true) + .build() + .unwrap(); + static ref ARG_REGEX: regex::Regex = regex::RegexBuilder::new( + r#" +( + ( + (? + [a-zA-Z0-9_]+ + \( + [^\)]+ + \) + ) + ) + | + ( + (? + [0-9x]+ + ( + \* + [0-9x]+ + )+ + ) + ) + | + ( + (?[-0-9]+) + ( + < + (?[^>]+) + > + ( + # (deleted) + \( + [^\)]+ + \) + )? + )? + (\ \/\*\ [A-Za-z0-9_\-\ \+\.\:\?]+\ \*\/)? + ) + | + ( + 0x + (?[a-f0-9]+) + (\ \/\*\ [A-Za-z0-9_\-\ \+\.\:\?]+ \*\/)? + ) + | + ( + \[ + (?.+) + \] + ) + | + ( + (?[A-Z_|~\[\]\ 0-9<]+) + ( + < + (?[^>]+) + > + )? + ) + | + ( + \{ + (? + ( + [a-z0-9_]+= + ( + ([^\{]+) + | + (\{[^\{]*\}) + ) + ,\ + )* + ( + ( + [a-z0-9_]+= + ( + ([^\{]+) + | + (\{[^\{]*\}) + ) + ) + | + \.{3} + )? + ) + \} + ) + | + ( + (?@)? + " + (?[^"]*) + " + ) +) +( + (,\ ) + | + $ +) +"# + ) + .ignore_whitespace(true) + .build() + .unwrap(); + static ref BYTE_REGEX: regex::bytes::Regex = + regex::bytes::Regex::new(r"\\x[0-9a-f]{2}").unwrap(); +} + +fn parse_buffer(s: &str) -> anyhow::Result> { + // Parse and replace '\x12' escaped bytes + let buf = BYTE_REGEX + .replace_all(s.as_bytes(), |cap: ®ex::bytes::Captures| { + let byte_match = cap.get(0).unwrap().as_bytes(); + let byte = u8::from_str_radix(str::from_utf8(&byte_match[2..]).unwrap(), 16).unwrap(); + vec![byte] + }) + .into_owned(); + Ok(buf) +} + +fn parse_argument(caps: ®ex::Captures) -> anyhow::Result { + if let Some(int) = caps.name("int") { + let metadata = caps + .name("int_metadata") + .map(|m| parse_buffer(m.as_str())) + .map_or(Ok(None), |v| v.map(Some))?; + Ok(SyscallArg::Integer { + value: IntegerExpression::Literal(int.as_str().parse()?), + metadata, + }) + } else if let Some(hex) = caps.name("int_hex") { + Ok(SyscallArg::Integer { + value: IntegerExpression::Literal(i128::from_str_radix(hex.as_str(), 16)?), + metadata: None, + }) + } else if let Some(const_) = caps.name("const_expr") { + // If you read this and are scared by the incomplete expression grammar parsing, lack of generic recursion, etc.: + // don't be, what strace outputs is actually limited to a few simple cases (or'ed flags, const, mask...) + let const_str = const_.as_str(); + if const_str.starts_with('~') { + assert!(!const_str.contains('|')); + assert_eq!(const_str.chars().nth(1), Some('[')); + assert_eq!(const_str.chars().last(), Some(']')); + let name = const_str[2..const_str.len() - 1] + .rsplit(' ') + .next() + .unwrap() + .to_string(); + Ok(SyscallArg::Integer { + value: IntegerExpression::BinaryNot(Box::new(IntegerExpression::NamedConst(name))), + metadata: None, + }) + } else { + let tokens = const_str.split('|').collect::>(); + if tokens.len() == 1 { + let metadata = caps + .name("const_expr_metadata") + .map(|m| parse_buffer(m.as_str())) + .map_or(Ok(None), |v| v.map(Some))?; + Ok(SyscallArg::Integer { + value: IntegerExpression::NamedConst(tokens[0].to_string()), + metadata, + }) + } else { + let int_tokens = tokens + .into_iter() + .map(|t| { + if t.starts_with("1<<") { + IntegerExpression::LeftBitShift { + bits: Box::new(IntegerExpression::Literal(1)), + shift: Box::new(IntegerExpression::NamedConst(t[3..].to_string())), + } + } else { + IntegerExpression::NamedConst(t.to_string()) + } + }) + .collect(); + Ok(SyscallArg::Integer { + value: IntegerExpression::BinaryOr(int_tokens), + metadata: None, + }) + } + } + } else if let Some(struct_) = caps.name("struct") { + let mut members = HashMap::new(); + let mut struct_ = struct_.as_str().to_string(); + while !struct_.is_empty() { + // dbg!(&struct_); + if struct_ == "..." { + // This should not append with our strace options, but still does, strace bug? + log::warn!("Truncated structure in strace output"); + break; + } + let (k, v) = struct_ + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("Unable to extract struct member name"))?; + // dbg!(&k); + // dbg!(&v); + let caps = ARG_REGEX + .captures(v) + .ok_or_else(|| anyhow::anyhow!("Unable to parse struct member value"))?; + let v = parse_argument(&caps)?; + // dbg!(&v); + members.insert(k.to_string(), v); + struct_ = struct_[k.len() + 1 + caps.get(0).unwrap().len()..struct_.len()].to_string(); + } + Ok(SyscallArg::Struct(members)) + } else if let Some(array) = caps.name("array") { + let members = ARG_REGEX + .captures_iter(array.as_str()) + .map(|a| parse_argument(&a)) + .collect::>()?; + Ok(SyscallArg::Array(members)) + } else if let Some(buf) = caps.name("buf") { + let buf = parse_buffer(buf.as_str())?; + let type_ = if caps.name("buf_abstract_path").is_some() { + BufferType::AbstractPath + } else { + BufferType::Unknown + }; + Ok(SyscallArg::Buffer { value: buf, type_ }) + } else if let Some(macro_) = caps.name("macro") { + let (name, args) = macro_.as_str().split_once('(').unwrap(); + let args = args[..args.len() - 1].to_string(); + let args = ARG_REGEX + .captures_iter(&args) + .map(|a| parse_argument(&a)) + .collect::>()?; + Ok(SyscallArg::Macro { + name: name.to_string(), + args, + }) + } else if let Some(multiplication) = caps.name("multiplication") { + let args = multiplication + .as_str() + .split('*') + .map(|a| -> anyhow::Result { + let arg = ARG_REGEX + .captures(a) + .ok_or_else(|| anyhow::anyhow!("Unexpected multiplication argument {a:?}"))?; + match parse_argument(&arg)? { + SyscallArg::Integer { value, .. } => Ok(value), + _ => Err(anyhow::anyhow!("Unexpected multiplication argument {a:?}")), + } + }) + .collect::>()?; + Ok(SyscallArg::Integer { + value: IntegerExpression::Multiplication(args), + metadata: None, + }) + } else { + unreachable!("Argument has no group match") + } +} + +fn parse_line(line: &str, unfinished_syscalls: &[Syscall]) -> anyhow::Result { + match LINE_REGEX.captures(line) { + Some(caps) => { + let pid = caps + .name("pid") + .unwrap() + .as_str() + .parse() + .map_err(|e| anyhow::Error::new(e).context("Failed to parse pid"))?; + + let rel_ts = caps + .name("rel_ts") + .unwrap() + .as_str() + .parse() + .map_err(|e| anyhow::Error::new(e).context("Failed to parse timestamp"))?; + + if let Some(name) = caps.name("name") { + let name = name.as_str().to_string(); + + let args = if let Some(arguments) = caps.name("arguments") { + ARG_REGEX + .captures_iter(arguments.as_str()) + .map(|a| parse_argument(&a)) + .collect::>()? + } else { + Vec::new() + }; + + let ret_val = if let Some(ret_val_int) = caps.name("ret_val_int") { + let s = ret_val_int.as_str(); + s.parse().map_err(|e| { + anyhow::Error::new(e) + .context(format!("Failed to parse integer return value: {s:?}")) + })? + } else if let Some(ret_val_hex) = caps.name("ret_val_hex") { + let s = ret_val_hex.as_str(); + SyscallRetVal::from_str_radix(s, 16).map_err(|e| { + anyhow::Error::new(e) + .context(format!("Failed to parse hexadecimal return value: {s:?}")) + })? + } else if caps.name("unfinished").is_some() { + return Ok(ParseResult::UnfinishedSyscall(Syscall { + pid, + rel_ts, + name, + args, + ret_val: SyscallRetVal::MAX, // Set dummy value we will replace + })); + } else { + unreachable!(); + }; + + let sc = Syscall { + pid, + rel_ts, + name, + args, + ret_val, + }; + Ok(ParseResult::Syscall(sc)) + } else if let Some(name_resumed) = caps.name("name_resumed").map(|c| c.as_str()) { + let ret_val = if let Some(ret_val_int) = caps.name("ret_val_int") { + let s = ret_val_int.as_str(); + s.parse().map_err(|e| { + anyhow::Error::new(e) + .context(format!("Failed to parse integer return value: {s:?}")) + })? + } else if let Some(ret_val_hex) = caps.name("ret_val_hex") { + let s = ret_val_hex.as_str(); + SyscallRetVal::from_str_radix(s, 16).map_err(|e| { + anyhow::Error::new(e) + .context(format!("Failed to parse hexadecimal return value: {s:?}")) + })? + } else { + unreachable!(); + }; + + let (unfinished_index, unfinished_sc) = unfinished_syscalls + .iter() + .enumerate() + .find(|(_i, sc)| (sc.name == name_resumed) && (sc.pid == pid)) + .ok_or_else(|| anyhow::anyhow!("Unabled to find first part of syscall"))?; + let sc = Syscall { + // Update return val and timestamp (to get return time instead of call time) + ret_val, + rel_ts, + ..unfinished_sc.clone() + }; + Ok(ParseResult::FinishedSyscall { + sc, + unfinished_index, + }) + } else { + unreachable!(); + } + } + None => Ok(ParseResult::IgnoredLine), + } +} + +impl Iterator for LogParser { + type Item = anyhow::Result; + + /// Parse strace output lines and yield syscalls + /// Ignore invalid lines, but bubble up errors if the regex matches and we fail subsequent parsing + fn next(&mut self) -> Option { + let sc = loop { + self.buf.clear(); + let line = match self.reader.read_line(&mut self.buf) { + Ok(0) => return None, // EOF + Ok(_) => self.buf.trim_end(), + Err(e) => return Some(Err(anyhow::Error::new(e).context("Failed to read line"))), + }; + + if line.ends_with(" +++") || line.ends_with(" ---") { + // Process exited, or signal received, not a syscall + continue; + } + + match parse_line(line, &self.unfinished_syscalls) { + Ok(ParseResult::Syscall(sc)) => { + log::trace!("Parsed line: {line:?}"); + break sc; + } + Ok(ParseResult::UnfinishedSyscall(sc)) => { + self.unfinished_syscalls.push(sc); + continue; + } + Ok(ParseResult::FinishedSyscall { + sc, + unfinished_index, + }) => { + self.unfinished_syscalls.swap_remove(unfinished_index); // I fucking love Rust <3 + break sc; + } + Ok(ParseResult::IgnoredLine) => { + log::warn!("Ignored line: {line:?}"); + continue; + } + Err(e) => { + log::error!("Failed to parse line: {line:?}"); + return Some(Err(e)); + } + }; + }; + Some(Ok(sc)) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::io::Cursor; + + use pretty_assertions::assert_eq; + + use super::*; + + #[test] + fn test_mmap() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "382944 0.000054 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f52a332e000", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 382944, + rel_ts: 0.000054, + name: "mmap".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::NamedConst("NULL".to_string()), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(8192), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("PROT_READ".to_string()), + IntegerExpression::NamedConst("PROT_WRITE".to_string()), + ]), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("MAP_PRIVATE".to_string()), + IntegerExpression::NamedConst("MAP_ANONYMOUS".to_string()), + ]), + metadata:None + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(-1), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + + ], + ret_val: 0x7f52a332e000 + }) + ); + + assert_eq!( + parse_line( + "601646 0.000011 mmap(0x7f2fce8dc000, 1396736, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x26000) = 0x7f2fce8dc000", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 601646, + rel_ts: 0.000011, + name: "mmap".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(0x7f2fce8dc000), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(1396736), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("PROT_READ".to_string()), + IntegerExpression::NamedConst("PROT_EXEC".to_string()), + ]), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("MAP_PRIVATE".to_string()), + IntegerExpression::NamedConst("MAP_FIXED".to_string()), + IntegerExpression::NamedConst("MAP_DENYWRITE".to_string()), + ]), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(3), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(0x26000), + metadata: None + }, + ], + ret_val: 0x7f2fce8dc000 + }) + ); + } + + #[test] + fn test_access() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "382944 0.000036 access(\"/etc/ld.so.preload\", R_OK) = -1 ENOENT (No such file or directory)", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 382944, + rel_ts: 0.000036, + name: "access".to_string(), + args: vec![ + SyscallArg::Buffer { + value: "/etc/ld.so.preload".as_bytes().to_vec(), + type_: BufferType::Unknown + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("R_OK".to_string()), + metadata: None, + }, + ], + ret_val: -1 + }) + ); + } + + #[test] + fn test_rt_sigaction() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "720313 0.000064 rt_sigaction(SIGTERM, {sa_handler=SIG_DFL, sa_mask=~[RTMIN RT_1], sa_flags=SA_RESTORER, sa_restorer=0x7f6da716c510}, NULL, 8) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 720313, + rel_ts: 0.000064, + name: "rt_sigaction".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::NamedConst("SIGTERM".to_string()), + metadata: None, + }, + SyscallArg::Struct(HashMap::from([ + ( + "sa_handler".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("SIG_DFL".to_string()), + metadata: None, + }, + ), + ( + "sa_mask".to_string(), + SyscallArg::Integer { + value: IntegerExpression::BinaryNot(Box::new(IntegerExpression::NamedConst("RT_1".to_string()))), + metadata: None, + }, + ), + ( + "sa_flags".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("SA_RESTORER".to_string()), + metadata: None, + }, + ), + ( + "sa_restorer".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0x7f6da716c510), + metadata: None + }, + ), + ])), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("NULL".to_string()), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(8), + metadata: None + }, + ], + ret_val: 0 + }) + ); + } + + #[test] + fn test_newfstatat() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "772627 0.000010 newfstatat(AT_FDCWD, \"/a/path\", {st_dev=makedev(0xfd, 0x1), st_ino=26427782, st_mode=S_IFDIR|0755, st_nlink=2, st_uid=1000, st_gid=1000, st_blksize=4096, st_blocks=112, st_size=53248, st_atime=1689948680 /* 2023-07-21T16:11:20.028467954+0200 */, st_atime_nsec=28467954, st_mtime=1692975712 /* 2023-08-25T17:01:52.252908565+0200 */, st_mtime_nsec=252908565, st_ctime=1692975712 /* 2023-08-25T17:01:52.252908565+0200 */, st_ctime_nsec=252908565}, 0) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 772627, + rel_ts: 0.000010, + name: "newfstatat".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AT_FDCWD".to_string()), + metadata: None, + }, + SyscallArg::Buffer { + value: "/a/path".as_bytes().to_vec(), + type_: BufferType::Unknown + }, + SyscallArg::Struct(HashMap::from([ + ( + "st_dev".to_string(), + SyscallArg::Macro { + name: "makedev".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(0xfd), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(1), + metadata: None, + }, + ], + }, + ), + ( + "st_ino".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(26427782), + metadata: None + }, + ), + ( + "st_mode".to_string(), + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("S_IFDIR".to_string()), + IntegerExpression::NamedConst("0755".to_string()) + ]), + metadata: None, + }, + ), + ( + "st_nlink".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(2), + metadata: None + }, + ), + ( + "st_uid".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(1000), + metadata: None + }, + ), + ( + "st_gid".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(1000), + metadata: None + }, + ), + ( + "st_blksize".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(4096), + metadata: None + }, + ), + ( + "st_blocks".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(112), + metadata: None + }, + ), + ( + "st_size".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(53248), + metadata: None + }, + ), + ( + "st_atime".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(1689948680), + metadata: None + }, + ), + ( + "st_atime_nsec".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(28467954), + metadata: None + }, + ), + ( + "st_mtime".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(1692975712), + metadata: None + }, + ), + ( + "st_mtime_nsec".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(252908565), + metadata: None + }, + ), + ( + "st_ctime".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(1692975712), + metadata: None + }, + ), + ( + "st_ctime_nsec".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(252908565), + metadata: None + }, + ), + ])), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + ], + ret_val: 0 + }) + ); + } + + #[test] + fn test_getrandom() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "815537 0.000017 getrandom(\"\\x42\\x18\\x81\\x90\\x40\\x63\\x1a\\x2c\", 8, GRND_NONBLOCK) = 8", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 815537, + rel_ts: 0.000017, + name: "getrandom".to_string(), + args: vec![ + SyscallArg::Buffer { + value: vec![0x42, 0x18, 0x81, 0x90, 0x40, 0x63, 0x1a, 0x2c], + type_: BufferType::Unknown + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(8), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("GRND_NONBLOCK".to_string()), + metadata: None, + }, + ], + ret_val: 8 + }) + ); + } + + #[test] + fn test_fstatfs() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "244841 0.000033 fstatfs(6, {f_type=EXT2_SUPER_MAGIC, f_bsize=4096, f_blocks=231830864, f_bfree=38594207, f_bavail=26799417, f_files=58957824, f_ffree=54942232, f_fsid={val=[0x511787a8, 0x92a74a52]}, f_namelen=255, f_frsize=4096, f_flags=ST_VALID|ST_NOATIME}) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 244841, + rel_ts: 0.000033, + name: "fstatfs".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(6), + metadata: None + }, + SyscallArg::Struct(HashMap::from([ + ( + "f_type".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("EXT2_SUPER_MAGIC".to_string()), + metadata: None, + }, + ), + ( + "f_bsize".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(4096), + metadata: None + }, + ), + ( + "f_blocks".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(231830864), + metadata: None + }, + ), + ( + "f_bfree".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(38594207), + metadata: None + }, + ), + ( + "f_bavail".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(26799417), + metadata: None + }, + ), + ( + "f_files".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(58957824), + metadata: None + }, + ), + ( + "f_ffree".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(54942232), + metadata: None + }, + ), + ( + "f_fsid".to_string(), + SyscallArg::Struct(HashMap::from([ + ( + "val".to_string(), + SyscallArg::Array(vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(1360496552), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(2460437074), + metadata: None + }, + ]) + ) + ])) + ), + ( + "f_namelen".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(255), + metadata: None + }, + ), + ( + "f_frsize".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(4096), + metadata: None + }, + ), + ( + "f_flags".to_string(), + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("ST_VALID".to_string()), + IntegerExpression::NamedConst("ST_NOATIME".to_string()) + ]), + metadata: None, + }, + ), + ])) + ], + ret_val: 0 + }) + ); + + assert_eq!( + parse_line( + "895683 0.000028 fstatfs(3, {f_type=PROC_SUPER_MAGIC, f_bsize=4096, f_blocks=0, f_bfree=0, f_bavail=0, f_files=0, f_ffree=0, f_fsid={val=[0, 0]}, f_namelen=255, f_frsize=4096, f_flags=ST_VALID|ST_NOSUID|ST_NODEV|ST_NOEXEC|ST_RELATIME}) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 895683, + rel_ts: 0.000028, + name: "fstatfs".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(3), + metadata: None + }, + SyscallArg::Struct(HashMap::from([ + ( + "f_type".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("PROC_SUPER_MAGIC".to_string()), + metadata: None, + }, + ), + ( + "f_bsize".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(4096), + metadata: None + }, + ), + ( + "f_blocks".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + ), + ( + "f_bfree".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + ), + ( + "f_bavail".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + ), + ( + "f_files".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + ), + ( + "f_ffree".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + ), + ( + "f_fsid".to_string(), + SyscallArg::Struct(HashMap::from([ + ( + "val".to_string(), + SyscallArg::Array(vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None + }, + ]) + ) + ])) + ), + ( + "f_namelen".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(255), + metadata: None + }, + ), + ( + "f_frsize".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(4096), + metadata: None + }, + ), + ( + "f_flags".to_string(), + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("ST_VALID".to_string()), + IntegerExpression::NamedConst("ST_NOSUID".to_string()), + IntegerExpression::NamedConst("ST_NODEV".to_string()), + IntegerExpression::NamedConst("ST_NOEXEC".to_string()), + IntegerExpression::NamedConst("ST_RELATIME".to_string()) + ]), + metadata: None, + }, + ), + ])) + ], + ret_val: 0 + }) + ); + } + + #[test] + fn test_open_relative() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "998518 0.000033 openat(AT_FDCWD<\\x2f\\x68\\x6f\\x6d\\x65\\x2f\\x6d\\x64\\x65\\x2f\\x73\\x72\\x63\\x2f\\x73\\x68\\x68>, \"\\x2e\\x2e\", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3<\\x2f\\x68\\x6f\\x6d\\x65\\x2f\\x6d\\x64\\x65\\x2f\\x73\\x72\\x63>", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 998518, + rel_ts: 0.000033, + name: "openat".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AT_FDCWD".to_string()), + metadata: Some(vec![0x2f, 0x68, 0x6f, 0x6d, 0x65, 0x2f, 0x6d, 0x64, 0x65, 0x2f, 0x73, 0x72, 0x63, 0x2f, 0x73, 0x68, 0x68]), + }, + SyscallArg::Buffer { + value: vec![0x2e, 0x2e], + type_: BufferType::Unknown, + }, + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("O_RDONLY".to_string()), + IntegerExpression::NamedConst("O_NONBLOCK".to_string()), + IntegerExpression::NamedConst("O_CLOEXEC".to_string()), + IntegerExpression::NamedConst("O_DIRECTORY".to_string()) + ]), + metadata: None, + }, + ], + ret_val: 3 + }) + ); + } + + #[test] + fn test_truncated() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "28707 0.000194 sendto(15<\\x73\\x6f\\x63\\x6b\\x65\\x74\\x3a\\x5b\\x35\\x34\\x31\\x38\\x32\\x31\\x33\\x5d>, [{nlmsg_len=20, nlmsg_type=RTM_GETADDR, nlmsg_flags=NLM_F_REQUEST|NLM_F_DUMP, nlmsg_seq=1694010548, nlmsg_pid=0}, {ifa_family=AF_UNSPEC, ...}], 20, 0, {sa_family=AF_NETLINK, nl_pid=0, nl_groups=00000000}, 12) = 20", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 28707, + rel_ts: 0.000194, + name: "sendto".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(15), + metadata: Some(vec![115, 111, 99, 107, 101, 116, 58, 91, 53, 52, 49, 56, 50, 49, 51, 93]) + }, + SyscallArg::Array(vec![ + SyscallArg::Struct(HashMap::from([ + ( + "nlmsg_len".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(20), + metadata: None, + }, + ), + ( + "nlmsg_type".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("RTM_GETADDR".to_string()), + metadata: None, + }, + ), + ( + "nlmsg_flags".to_string(), + SyscallArg::Integer { + value: IntegerExpression::BinaryOr(vec![ + IntegerExpression::NamedConst("NLM_F_REQUEST".to_string()), + IntegerExpression::NamedConst("NLM_F_DUMP".to_string()), + ]), + metadata: None, + }, + ), + ( + "nlmsg_seq".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(1694010548), + metadata: None, + }, + ), + ( + "nlmsg_pid".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None, + }, + ), + ])), + SyscallArg::Struct(HashMap::from([ + ( + "ifa_family".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AF_UNSPEC".to_string()), + metadata: None, + }, + ), + ])), + ]), + SyscallArg::Integer { + value: IntegerExpression::Literal(20), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None, + }, + SyscallArg::Struct(HashMap::from([ + ( + "sa_family".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AF_NETLINK".to_string()), + metadata: None, + }, + ), + ( + "nl_pid".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None, + }, + ), + ( + "nl_groups".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None, + }, + ), + ])), + SyscallArg::Integer { + value: IntegerExpression::Literal(12), + metadata: None, + }, + ], + ret_val: 20 + }) + ); + } + + #[test] + fn test_bind() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "688129 0.000023 bind(4<\\x73\\x6f\\x63\\x6b\\x65\\x74\\x3a\\x5b\\x34\\x31\\x38\\x34\\x35\\x32\\x32\\x5d>, {sa_family=AF_UNIX, sun_path=@\"\\x62\\x31\\x39\\x33\\x64\\x30\\x62\\x30\\x63\\x63\\x64\\x37\\x30\\x35\\x66\\x39\\x2f\\x62\\x75\\x73\\x2f\\x73\\x79\\x73\\x74\\x65\\x6d\\x63\\x74\\x6c\\x2f\"}, 34) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 688129, + rel_ts: 0.000023, + name: "bind".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(4), + metadata: Some("socket:[4184522]".as_bytes().to_vec()) + }, + SyscallArg::Struct(HashMap::from([ + ( + "sa_family".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AF_UNIX".to_string()), + metadata: None, + }, + ), + ( + "sun_path".to_string(), + SyscallArg::Buffer { + value: "b193d0b0ccd705f9/bus/systemctl/".as_bytes().to_vec(), + type_: BufferType::AbstractPath, + }, + ), + ])), + SyscallArg::Integer { + value: IntegerExpression::Literal(34), + metadata: None, + }, + ], + ret_val: 0 + }) + ); + + assert_eq!( + parse_line( + "132360 0.000022 bind(6<\\x73\\x6f\\x63\\x6b\\x65\\x74\\x3a\\x5b\\x38\\x31\\x35\\x36\\x39\\x33\\x5d>, {sa_family=AF_INET, sin_port=htons(8025), sin_addr=inet_addr(\"\\x31\\x32\\x37\\x2e\\x30\\x2e\\x30\\x2e\\x31\")}, 16) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 132360, + rel_ts: 0.000022, + name: "bind".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(6), + metadata: Some(vec![115, 111, 99, 107, 101, 116, 58, 91, 56, 49, 53, 54, 57, 51, 93]), + }, + SyscallArg::Struct(HashMap::from([ + ( + "sa_family".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AF_INET".to_string()), + metadata: None, + }, + ), + ( + "sin_port".to_string(), + SyscallArg::Macro { + name: "htons".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(8025), + metadata: None, + }, + ], + } + ), + ( + "sin_addr".to_string(), + SyscallArg::Macro { + name: "inet_addr".to_string(), + args: vec![ + SyscallArg::Buffer { + value: vec![49, 50, 55, 46, 48, 46, 48, 46, 49], + type_: BufferType::Unknown, + }, + ], + } + ), + ])), + SyscallArg::Integer { + value: IntegerExpression::Literal(16), + metadata: None, + }, + ], + ret_val: 0 + }) + ); + } + + #[test] + fn test_multiplication() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "85195 0.000038 prlimit64(0, RLIMIT_NOFILE, {rlim_cur=512*1024, rlim_max=512*1024}, NULL) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 85195, + rel_ts: 0.000038, + name: "prlimit64".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("RLIMIT_NOFILE".to_string()), + metadata: None, + }, + SyscallArg::Struct(HashMap::from([ + ( + "rlim_cur".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Multiplication(vec![ + IntegerExpression::Literal(512), + IntegerExpression::Literal(1024), + ]), + metadata: None, + }, + ), + ( + "rlim_max".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Multiplication(vec![ + IntegerExpression::Literal(512), + IntegerExpression::Literal(1024), + ]), + metadata: None, + }, + ), + ])), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("NULL".to_string()), + metadata: None, + }, + ], + ret_val: 0 + }) + ); + } + + #[test] + fn test_epoll() { + let _ = simple_logger::SimpleLogger::new().init(); + + assert_eq!( + parse_line( + "114586 0.000075 epoll_ctl(3<\\x61\\x6e\\x6f\\x6e\\x5f\\x69\\x6e\\x6f\\x64\\x65\\x3a\\x5b\\x65\\x76\\x65\\x6e\\x74\\x70\\x6f\\x6c\\x6c\\x5d>, EPOLL_CTL_ADD, 4<\\x73\\x6f\\x63\\x6b\\x65\\x74\\x3a\\x5b\\x37\\x33\\x31\\x35\\x39\\x38\\x5d>, {events=EPOLLIN, data={u32=4, u64=4}}) = 0", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 114586, + rel_ts: 0.000075, + name: "epoll_ctl".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(3), + metadata: Some(vec![97, 110, 111, 110, 95, 105, 110, 111, 100, 101, 58, 91, 101, 118, 101, 110, 116, 112, 111, 108, 108, 93]), + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("EPOLL_CTL_ADD".to_string()), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(4), + metadata: Some(vec![115, 111, 99, 107, 101, 116, 58, 91, 55, 51, 49, 53, 57, 56, 93]), + }, + SyscallArg::Struct(HashMap::from([ + ( + "events".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("EPOLLIN".to_string()), + metadata: None, + }, + ), + ( + "data".to_string(), + SyscallArg::Struct(HashMap::from([ + ( + "u32".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(4), + metadata: None, + } + ), + ( + "u64".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(4), + metadata: None, + } + ), + ])) + ), + ])), + ], + ret_val: 0 + }) + ); + + assert_eq!( + parse_line( + "3487 0.000130 epoll_pwait(4<\\x61\\x6e\\x6f\\x6e\\x5f\\x69\\x6e\\x6f\\x64\\x65\\x3a\\x5b\\x65\\x76\\x65\\x6e\\x74\\x70\\x6f\\x6c\\x6c\\x5d>, [{events=EPOLLOUT, data={u32=833093633, u64=9163493471957811201}}, {events=EPOLLOUT, data={u32=800587777, u64=9163493471925305345}}], 128, 0, NULL, 0) = 2", + &[] + ).unwrap(), + ParseResult::Syscall(Syscall { + pid: 3487, + rel_ts: 0.000130, + name: "epoll_pwait".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(4), + metadata: Some(vec![0x61, 0x6e, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x6f, 0x64, 0x65, 0x3a, 0x5b, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x70, 0x6f, 0x6c, 0x6c, 0x5d]), + }, + SyscallArg::Array(vec![ + SyscallArg::Struct(HashMap::from([ + ( + "events".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("EPOLLOUT".to_string()), + metadata: None, + }, + ), + ( + "data".to_string(), + SyscallArg::Struct(HashMap::from([ + ( + "u32".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(833093633), + metadata: None, + } + ), + ( + "u64".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(9163493471957811201), + metadata: None, + } + ), + ])) + ), + ])), + SyscallArg::Struct(HashMap::from([ + ( + "events".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("EPOLLOUT".to_string()), + metadata: None, + }, + ), + ( + "data".to_string(), + SyscallArg::Struct(HashMap::from([ + ( + "u32".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(800587777), + metadata: None, + } + ), + ( + "u64".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(9163493471925305345), + metadata: None, + } + ), + ])) + ), + ])), + ]), + SyscallArg::Integer { + value: IntegerExpression::Literal(128), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("NULL".to_string()), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::Literal(0), + metadata: None, + }, + ], + ret_val: 2 + }) + ); + } + + #[test] + fn test_interleave() { + let _ = simple_logger::SimpleLogger::new().init(); + + let lines = Cursor::new( + "1 0.000001 select(4, [3], NULL, NULL, NULL +2 0.000002 clock_gettime(CLOCK_REALTIME, {tv_sec=1130322148, tv_nsec=3977000}) = 0 +1 0.000003 <... select resumed> ) = 1 (in [3])" + .as_bytes() + .to_vec(), + ); + let parser = LogParser::new(Box::new(lines)).unwrap(); + let syscalls: Vec = parser.into_iter().collect::>().unwrap(); + + assert_eq!( + syscalls, + vec![ + Syscall { + pid: 2, + rel_ts: 0.000002, + name: "clock_gettime".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::NamedConst("CLOCK_REALTIME".to_string()), + metadata: None, + }, + SyscallArg::Struct(HashMap::from([ + ( + "tv_sec".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(1130322148), + metadata: None, + }, + ), + ( + "tv_nsec".to_string(), + SyscallArg::Integer { + value: IntegerExpression::Literal(3977000), + metadata: None, + }, + ), + ])), + ], + ret_val: 0 + }, + Syscall { + pid: 1, + rel_ts: 0.000003, + name: "select".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(4), + metadata: None, + }, + SyscallArg::Array(vec![SyscallArg::Integer { + value: IntegerExpression::Literal(3), + metadata: None, + },]), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("NULL".to_string()), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("NULL".to_string()), + metadata: None, + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("NULL".to_string()), + metadata: None, + }, + ], + ret_val: 1 + } + ] + ); + } +} + +#[cfg(all(feature = "nightly", test))] +mod benchs { + extern crate test; + + use super::*; + + use std::iter; + + use test::Bencher; + + #[bench] + fn bench_parse_buffer(b: &mut Bencher) { + let s = format!( + "\"{}\"", + iter::repeat_with(|| format!("\\x{:02x}", fastrand::u8(..))) + .take(512) + .collect::>() + .join("") + ); + + b.iter(|| { + parse_buffer(&s).unwrap(); + }); + } +} diff --git a/src/strace/run.rs b/src/strace/run.rs new file mode 100644 index 0000000..f167466 --- /dev/null +++ b/src/strace/run.rs @@ -0,0 +1,75 @@ +//! Strace invocation code + +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; + +use crate::strace::parser::LogParser; + +pub struct Strace { + /// Strace process + process: Child, + /// Temp dir for pipe location + pipe_dir: tempfile::TempDir, +} + +impl Strace { + pub fn run(command: &[&str]) -> anyhow::Result { + // Create named pipe + let pipe_dir = tempfile::tempdir()?; + let pipe_path = Self::pipe_path(&pipe_dir); + nix::unistd::mkfifo(&pipe_path, nix::sys::stat::Mode::from_bits(0o600).unwrap())?; + + // Start process + let child = Command::new("strace") + .args([ + "--daemonize=grandchild", + "--relative-timestamps", + "--follow-forks", + // TODO APPROXIMATION this can make us miss interesting stuff like open with O_EXCL|O_CREAT which + // returns -1 because file exists + "--successful-only", + "--strings-in-hex=all", + // Despite this, some structs are still truncated + "-e", + "abbrev=none", + // "-e", + // "read=all", + // "-e", + // "write=all", + "-e", + "decode-fds=path", + "--output-append-mode", + "-o", + pipe_path.to_str().unwrap(), + "--", + ]) + .args(command) + .env("LANG", "C") // avoids locale side effects + .stdin(Stdio::null()) + .spawn()?; + + Ok(Self { + process: child, + pipe_dir, + }) + } + + fn pipe_path(dir: &tempfile::TempDir) -> PathBuf { + dir.path().join("strace.pipe") + } + + pub fn log_lines(&self) -> anyhow::Result { + let pipe_path = Self::pipe_path(&self.pipe_dir); + let reader = BufReader::new(File::open(pipe_path)?); + LogParser::new(Box::new(reader)) + } +} + +impl Drop for Strace { + fn drop(&mut self) { + let _ = self.process.kill(); + let _ = self.process.wait(); + } +} diff --git a/src/summarize.rs b/src/summarize.rs new file mode 100644 index 0000000..a2bb68c --- /dev/null +++ b/src/summarize.rs @@ -0,0 +1,429 @@ +//! Summarize program syscalls into higher level action + +use std::collections::{HashMap, HashSet}; +use std::ffi::OsStr; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; + +use lazy_static::lazy_static; + +use crate::strace::{BufferType, IntegerExpression, Syscall, SyscallArg}; + +/// A high level program runtime action +/// This does *not* map 1-1 with a syscall, and does *not* necessarily respect chronology +#[derive(Debug, Eq, PartialEq)] +pub enum ProgramAction { + /// Path was accessed (open, stat'ed, read...) + Read(PathBuf), + /// Path was written to (data, metadata, path removal...) + Write(PathBuf), + /// Path was created + Create(PathBuf), + /// Names of the syscalls made by the program + Syscalls(HashSet), +} + +struct OpenSyscallInfo { + relfd_idx: Option, + path_idx: usize, + flags_idx: usize, +} + +struct RenameSyscallInfo { + relfd_src_idx: Option, + path_src_idx: usize, + relfd_dst_idx: Option, + path_dst_idx: usize, + flags_idx: Option, +} + +lazy_static! { + // + // For some reference on syscalls, see: + // - https://man7.org/linux/man-pages/man2/syscalls.2.html + // - https://filippo.io/linux-syscall-table/ + // - https://linasm.sourceforge.net/docs/syscalls/filesystem.php + // + static ref OPEN_SYSCALL: HashMap<&'static str, OpenSyscallInfo> = HashMap::from([ + ( + "open", + OpenSyscallInfo { + relfd_idx: None, + path_idx: 0, + flags_idx: 1 + } + ), + ( + "openat", + OpenSyscallInfo { + relfd_idx: Some(0), + path_idx: 1, + flags_idx: 2 + } + ) + ]); + static ref RENAME_SYSCALL: HashMap<&'static str, RenameSyscallInfo> = HashMap::from([ + ( + "rename", + RenameSyscallInfo { + relfd_src_idx: None, + path_src_idx: 0, + relfd_dst_idx: None, + path_dst_idx: 1, + flags_idx: None, + } + ), + ( + "renameat", + RenameSyscallInfo { + relfd_src_idx: Some(0), + path_src_idx: 1, + relfd_dst_idx: Some(2), + path_dst_idx: 3, + flags_idx: None, + } + ), + ( + "renameat2", + RenameSyscallInfo { + relfd_src_idx: Some(0), + path_src_idx: 1, + relfd_dst_idx: Some(2), + path_dst_idx: 3, + flags_idx: Some(4), + } + ) + ]); +} + +/// Resolve relative path if possible, and normalize it +fn resolve_path(path: &Path, relfd_idx: Option, syscall: &Syscall) -> Option { + let path = if path.is_relative() { + let metadata = relfd_idx + .and_then(|idx| syscall.args.get(idx)) + .and_then(|a| a.metadata()); + if let Some(metadata) = metadata { + if is_fd_pseudo_path(metadata) { + return None; + } + let rel_path = PathBuf::from(OsStr::from_bytes(metadata)); + rel_path.join(path) + } else { + return None; + } + } else { + path.to_path_buf() + }; + // TODO APPROXIMATION + // canonicalize relies on the FS state at profiling time which may have changed + // and may follow links, therefore lead to different filesystem actions + Some(path.canonicalize().unwrap_or(path)) +} + +lazy_static! { + static ref FD_PSEUDO_PATH_REGEX: regex::bytes::Regex = + regex::bytes::Regex::new(r"^[a-z]+:\[[0-9a-z]+\]/?$").unwrap(); +} + +fn is_fd_pseudo_path(path: &[u8]) -> bool { + FD_PSEUDO_PATH_REGEX.is_match(path) +} + +/// Extract path for socket address structure if it's a non abstract one +fn socket_address_uds_path( + members: &HashMap, + syscall: &Syscall, +) -> Option { + if let Some(SyscallArg::Buffer { + value: b, + type_: BufferType::Unknown, + }) = members.get("sun_path") + { + resolve_path(&PathBuf::from(OsStr::from_bytes(b)), None, syscall) + } else { + None + } +} + +pub fn summarize(syscalls: I) -> anyhow::Result> +where + I: IntoIterator>, +{ + let mut actions = Vec::new(); + let mut stats: HashMap = HashMap::new(); + for syscall in syscalls { + let syscall = syscall?; + log::trace!("{syscall:?}"); + stats + .entry(syscall.name.clone()) + .and_modify(|c| *c += 1) + .or_insert(1); + let name = syscall.name.as_str(); + + if let Some(open_info) = OPEN_SYSCALL.get(name) { + let (mut path, flags) = if let ( + Some(SyscallArg::Buffer { + value: b, + type_: BufferType::Unknown, + }), + Some(SyscallArg::Integer { value: e, .. }), + ) = ( + syscall.args.get(open_info.path_idx), + syscall.args.get(open_info.flags_idx), + ) { + (PathBuf::from(OsStr::from_bytes(b)), e) + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + + path = if let Some(path) = resolve_path(&path, open_info.relfd_idx, &syscall) { + path + } else { + continue; + }; + + if flags.is_flag_set("O_CREAT") { + actions.push(ProgramAction::Create(path.clone())); + } + if flags.is_flag_set("O_WRONLY") + || flags.is_flag_set("O_RDWR") + || flags.is_flag_set("O_TRUNC") + { + actions.push(ProgramAction::Write(path.clone())); + } + if !flags.is_flag_set("O_WRONLY") { + actions.push(ProgramAction::Read(path)); + } + } else if let Some(rename_info) = RENAME_SYSCALL.get(name) { + let (mut path_src, mut path_dst) = if let ( + Some(SyscallArg::Buffer { + value: b1, + type_: BufferType::Unknown, + }), + Some(SyscallArg::Buffer { + value: b2, + type_: BufferType::Unknown, + }), + ) = ( + syscall.args.get(rename_info.path_src_idx), + syscall.args.get(rename_info.path_dst_idx), + ) { + ( + PathBuf::from(OsStr::from_bytes(b1)), + PathBuf::from(OsStr::from_bytes(b2)), + ) + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + + (path_src, path_dst) = if let (Some(path_src), Some(path_dst)) = ( + resolve_path(&path_src, rename_info.relfd_src_idx, &syscall), + resolve_path(&path_dst, rename_info.relfd_dst_idx, &syscall), + ) { + (path_src, path_dst) + } else { + continue; + }; + + let exchange = if let Some(flags_idx) = rename_info.flags_idx { + let flags = if let Some(SyscallArg::Integer { value: flags, .. }) = + syscall.args.get(flags_idx) + { + flags + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + + flags.is_flag_set("RENAME_EXCHANGE") + } else { + false + }; + + actions.push(ProgramAction::Read(path_src.clone())); + actions.push(ProgramAction::Write(path_src.clone())); + if exchange { + actions.push(ProgramAction::Read(path_dst.clone())); + } else { + actions.push(ProgramAction::Create(path_dst.clone())); + } + actions.push(ProgramAction::Write(path_dst.clone())); + } else if name.starts_with("getdents") { + // TODO factorize with newfstatat, handle other stat variants? + let mut path = syscall + .args + .get(0) + .and_then(|a| a.metadata()) + .map(|m| PathBuf::from(OsStr::from_bytes(m))) + .ok_or_else(|| anyhow::anyhow!("Unexpected args for {name}"))?; + path = if let Some(path) = resolve_path(&path, None, &syscall) { + path + } else { + continue; + }; + actions.push(ProgramAction::Read(path)); + } else if name.starts_with("newfstatat") { + let mut path = if let Some(SyscallArg::Buffer { + value: b, + type_: BufferType::Unknown, + }) = syscall.args.get(1) + { + PathBuf::from(OsStr::from_bytes(b)) + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + path = if let Some(path) = resolve_path(&path, Some(0), &syscall) { + path + } else { + continue; + }; + actions.push(ProgramAction::Read(path)); + } else if ["bind", "connect"].contains(&name) { + // TODO other network syscalls that can handle UDS (sendto, sendmsg, recvfrom, recvmsg) + + let (af, addr) = if let Some(SyscallArg::Struct(members)) = syscall.args.get(1) { + let af = if let Some(SyscallArg::Integer { + value: IntegerExpression::NamedConst(af), + .. + }) = members.get("sa_family") + { + af + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + (af.as_str(), members) + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + + #[allow(clippy::single_match)] + match af { + "AF_UNIX" => { + if let Some(path) = socket_address_uds_path(addr, &syscall) { + actions.push(ProgramAction::Read(path)); + }; + } + _ => (), + } + } + } + + // Almost free optimization + actions.dedup(); + + // Create single action with all syscalls for efficient handling of seccomp filters + actions.push(ProgramAction::Syscalls(stats.keys().cloned().collect())); + + // Report stats + let mut syscall_names = stats.keys().collect::>(); + syscall_names.sort(); + for syscall_name in syscall_names { + let count = stats.get(syscall_name).unwrap(); + log::debug!("{:20} {: >12}", format!("{syscall_name}:"), count); + } + + Ok(actions) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::strace::*; + + #[test] + fn test_is_socket_or_pipe_pseudo_path() { + assert!(!is_fd_pseudo_path("plop".as_bytes())); + assert!(is_fd_pseudo_path("pipe:[12334]".as_bytes())); + assert!(is_fd_pseudo_path("socket:[1234]/".as_bytes())); + } + + #[test] + fn test_relative_rename() { + let _ = simple_logger::SimpleLogger::new().init(); + + let temp_dir_src = tempfile::tempdir().unwrap(); + let temp_dir_dst = tempfile::tempdir().unwrap(); + let syscalls = [Ok(Syscall { + pid: 1068781, + rel_ts: 0.000083, + name: "renameat".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AT_FDCWD".to_string()), + metadata: Some(temp_dir_src.path().as_os_str().as_bytes().to_vec()), + }, + SyscallArg::Buffer { + value: "a".as_bytes().to_vec(), + type_: BufferType::Unknown, + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AT_FDCWD".to_string()), + metadata: Some(temp_dir_dst.path().as_os_str().as_bytes().to_vec()), + }, + SyscallArg::Buffer { + value: "b".as_bytes().to_vec(), + type_: BufferType::Unknown, + }, + SyscallArg::Integer { + value: IntegerExpression::NamedConst("RENAME_NOREPLACE".to_string()), + metadata: None, + }, + ], + ret_val: 0, + })]; + assert_eq!( + summarize(syscalls).unwrap(), + vec![ + ProgramAction::Read(temp_dir_src.path().join("a")), + ProgramAction::Write(temp_dir_src.path().join("a")), + ProgramAction::Create(temp_dir_dst.path().join("b")), + ProgramAction::Write(temp_dir_dst.path().join("b")), + ProgramAction::Syscalls(["renameat".to_string()].into()) + ] + ); + } + + #[test] + fn test_connect_uds() { + let _ = simple_logger::SimpleLogger::new().init(); + + let syscalls = [Ok(Syscall { + pid: 598056, + rel_ts: 0.000036, + name: "connect".to_string(), + args: vec![ + SyscallArg::Integer { + value: IntegerExpression::Literal(4), + metadata: Some("/run/user/1000/systemd/private".as_bytes().to_vec()), + }, + SyscallArg::Struct(HashMap::from([ + ( + "sa_family".to_string(), + SyscallArg::Integer { + value: IntegerExpression::NamedConst("AF_UNIX".to_string()), + metadata: None, + }, + ), + ( + "sun_path".to_string(), + SyscallArg::Buffer { + value: "/run/user/1000/systemd/private".as_bytes().to_vec(), + type_: BufferType::Unknown, + }, + ), + ])), + SyscallArg::Integer { + value: IntegerExpression::Literal(33), + metadata: None, + }, + ], + ret_val: 0, + })]; + assert_eq!( + summarize(syscalls).unwrap(), + vec![ + ProgramAction::Read("/run/user/1000/systemd/private".into()), + ProgramAction::Syscalls(["connect".to_string()].into()) + ] + ); + } +} diff --git a/src/systemd/mod.rs b/src/systemd/mod.rs new file mode 100644 index 0000000..7876902 --- /dev/null +++ b/src/systemd/mod.rs @@ -0,0 +1,23 @@ +//! Systemd code + +mod options; +mod resolver; +mod service; +mod version; + +pub use options::build_options; +pub use resolver::resolve; +pub use service::Service; +pub use version::{KernelVersion, SystemdVersion}; + +const START_OPTION_OUTPUT_SNIPPET: &str = "-------- Start of suggested service options --------"; +const END_OPTION_OUTPUT_SNIPPET: &str = "-------- End of suggested service options --------"; + +pub fn report_options(opts: Vec) { + // Report (not through logging facility because we may need to parse it back from service logs) + println!("{}", START_OPTION_OUTPUT_SNIPPET); + for opt in opts { + println!("{opt}"); + } + println!("{}", END_OPTION_OUTPUT_SNIPPET); +} diff --git a/src/systemd/options.rs b/src/systemd/options.rs new file mode 100644 index 0000000..56ff200 --- /dev/null +++ b/src/systemd/options.rs @@ -0,0 +1,986 @@ +//! Systemd option modeling + +use std::collections::{HashMap, HashSet}; +use std::fmt; +use std::iter; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use lazy_static::lazy_static; + +use crate::systemd::{KernelVersion, SystemdVersion}; + +/// Systemd option with its possibles values, and their effect +#[derive(Debug)] +pub struct OptionDescription { + pub name: String, + pub possible_values: Vec, +} + +impl fmt::Display for OptionDescription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.name.fmt(f) + } +} + +/// Systemd option value +#[derive(Debug, Clone)] +pub enum OptionValue { + Boolean(bool), // In most case we only model the 'true' value, because false is no-op and the default + String(String), // enum-like, or free string + DenyList(Vec), +} + +impl FromStr for OptionValue { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "true" => Ok(OptionValue::Boolean(true)), + "false" => Ok(OptionValue::Boolean(false)), + _ => Ok(OptionValue::String(s.to_string())), + } + } +} + +/// A systemd option value and its effects +#[derive(Debug)] +pub struct OptionValueDescription { + pub value: OptionValue, + pub desc: OptionEffect, +} + +/// The effects a systemd option has if enabled +#[derive(Debug, Clone)] +pub enum OptionEffect { + /// Option has several mutually exclusive possible values + Simple(OptionValueEffect), + /// Option has several possible values, that can be combined to stack effects + Cumulative(Vec), +} + +#[derive(Debug, Clone)] +pub enum PathDescription { + Base { + base: PathBuf, + exceptions: Vec, + }, + Pattern(regex::bytes::Regex), +} + +impl PathDescription { + pub fn matches(&self, path: &Path) -> bool { + assert!(path.is_absolute(), "{path:?}"); + match self { + PathDescription::Base { base, exceptions } => { + path.starts_with(base) && !exceptions.iter().any(|e| path.starts_with(e)) + } + PathDescription::Pattern(r) => r.is_match(path.as_os_str().as_bytes()), + } + } +} + +#[derive(Debug, Clone)] +pub enum OptionValueEffect { + /// Mount path as read only + DenyWrite(PathDescription), + /// Mount an empty tmpfs under given directory + Hide(PathDescription), + /// Deny a whole class of syscalls + /// See https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L306 + /// for the content of each class + DenySyscall { class: String }, + /// Union of multiple effects + Multiple(Vec), +} + +/// A systemd option with a value, as would be present in a config file +pub struct OptionWithValue { + pub name: String, + pub value: OptionValue, +} + +impl FromStr for OptionWithValue { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (name, value) = s + .split_once('=') + .ok_or_else(|| anyhow::anyhow!("Missing '=' char in {s:?}"))?; + + Ok(Self { + name: name.to_string(), + value: value.parse().unwrap(), // never fails + }) + } +} + +impl fmt::Display for OptionWithValue { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}=", self.name)?; + match &self.value { + OptionValue::Boolean(value) => { + write!(f, "{}", if *value { "true" } else { "false" }) + } + OptionValue::String(value) => { + write!(f, "{value}") + } + OptionValue::DenyList(values) => { + write!( + f, + "~{}", + values + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(" ") + ) + } + } + } +} + +lazy_static! { + pub static ref SYSCALL_CLASSES: HashMap> = HashMap::from([ + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L374 + "@aio".to_string(), + HashSet::from([ + "io_cancel".to_string(), + "io_destroy".to_string(), + "io_getevents".to_string(), + "io_pgetevents".to_string(), + "io_pgetevents_time64".to_string(), + "io_setup".to_string(), + "io_submit".to_string(), + "io_uring_enter".to_string(), + "io_uring_register".to_string(), + "io_uring_setup".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L389 + "@basic-io".to_string(), + HashSet::from([ + "_llseek".to_string(), + "close".to_string(), + "close_range".to_string(), + "dup".to_string(), + "dup2".to_string(), + "dup3".to_string(), + "lseek".to_string(), + "pread64".to_string(), + "preadv".to_string(), + "preadv2".to_string(), + "pwrite64".to_string(), + "pwritev".to_string(), + "pwritev2".to_string(), + "read".to_string(), + "readv".to_string(), + "write".to_string(), + "writev".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L411 + "@chown".to_string(), + HashSet::from([ + "chown".to_string(), + "chown32".to_string(), + "fchown".to_string(), + "fchown32".to_string(), + "fchownat".to_string(), + "lchown".to_string(), + "lchown32".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L423 + "@clock".to_string(), + HashSet::from([ + "adjtimex".to_string(), + "clock_adjtime".to_string(), + "clock_adjtime64".to_string(), + "clock_settime".to_string(), + "clock_settime64".to_string(), + "settimeofday".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L434 + "@cpu-emulation".to_string(), + HashSet::from([ + "modify_ldt".to_string(), + "subpage_prot".to_string(), + "switch_endian".to_string(), + "vm86".to_string(), + "vm86old".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L444 + "@debug".to_string(), + HashSet::from([ + "lookup_dcookie".to_string(), + "perf_event_open".to_string(), + "pidfd_getfd".to_string(), + "ptrace".to_string(), + "rtas".to_string(), + "s390_runtime_instr".to_string(), + "sys_debug_setcontext".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L456 + "@file-system".to_string(), + HashSet::from([ + "access".to_string(), + "chdir".to_string(), + "chmod".to_string(), + "close".to_string(), + "creat".to_string(), + "faccessat".to_string(), + "faccessat2".to_string(), + "fallocate".to_string(), + "fchdir".to_string(), + "fchmod".to_string(), + "fchmodat".to_string(), + "fcntl".to_string(), + "fcntl64".to_string(), + "fgetxattr".to_string(), + "flistxattr".to_string(), + "fremovexattr".to_string(), + "fsetxattr".to_string(), + "fstat".to_string(), + "fstat64".to_string(), + "fstatat64".to_string(), + "fstatfs".to_string(), + "fstatfs64".to_string(), + "ftruncate".to_string(), + "ftruncate64".to_string(), + "futimesat".to_string(), + "getcwd".to_string(), + "getdents".to_string(), + "getdents64".to_string(), + "getxattr".to_string(), + "inotify_add_watch".to_string(), + "inotify_init".to_string(), + "inotify_init1".to_string(), + "inotify_rm_watch".to_string(), + "lgetxattr".to_string(), + "link".to_string(), + "linkat".to_string(), + "listxattr".to_string(), + "llistxattr".to_string(), + "lremovexattr".to_string(), + "lsetxattr".to_string(), + "lstat".to_string(), + "lstat64".to_string(), + "mkdir".to_string(), + "mkdirat".to_string(), + "mknod".to_string(), + "mknodat".to_string(), + "newfstatat".to_string(), + "oldfstat".to_string(), + "oldlstat".to_string(), + "oldstat".to_string(), + "open".to_string(), + "openat".to_string(), + "openat2".to_string(), + "readlink".to_string(), + "readlinkat".to_string(), + "removexattr".to_string(), + "rename".to_string(), + "renameat".to_string(), + "renameat2".to_string(), + "rmdir".to_string(), + "setxattr".to_string(), + "stat".to_string(), + "stat64".to_string(), + "statfs".to_string(), + "statfs64".to_string(), + "statx".to_string(), + "symlink".to_string(), + "symlinkat".to_string(), + "truncate".to_string(), + "truncate64".to_string(), + "unlink".to_string(), + "unlinkat".to_string(), + "utime".to_string(), + "utimensat".to_string(), + "utimensat_time64".to_string(), + "utimes".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L537 + "@io-event".to_string(), + HashSet::from([ + "_newselect".to_string(), + "epoll_create".to_string(), + "epoll_create1".to_string(), + "epoll_ctl".to_string(), + "epoll_ctl_old".to_string(), + "epoll_pwait".to_string(), + "epoll_pwait2".to_string(), + "epoll_wait".to_string(), + "epoll_wait_old".to_string(), + "eventfd".to_string(), + "eventfd2".to_string(), + "poll".to_string(), + "ppoll".to_string(), + "ppoll_time64".to_string(), + "pselect6".to_string(), + "pselect6_time64".to_string(), + "select".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L559 + "@ipc".to_string(), + HashSet::from([ + "ipc".to_string(), + "memfd_create".to_string(), + "mq_getsetattr".to_string(), + "mq_notify".to_string(), + "mq_open".to_string(), + "mq_timedreceive".to_string(), + "mq_timedreceive_time64".to_string(), + "mq_timedsend".to_string(), + "mq_timedsend_time64".to_string(), + "mq_unlink".to_string(), + "msgctl".to_string(), + "msgget".to_string(), + "msgrcv".to_string(), + "msgsnd".to_string(), + "pipe".to_string(), + "pipe2".to_string(), + "process_madvise".to_string(), + "process_vm_readv".to_string(), + "process_vm_writev".to_string(), + "semctl".to_string(), + "semget".to_string(), + "semop".to_string(), + "semtimedop".to_string(), + "semtimedop_time64".to_string(), + "shmat".to_string(), + "shmctl".to_string(), + "shmdt".to_string(), + "shmget".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L592 + "@keyring".to_string(), + HashSet::from([ + "add_key".to_string(), + "keyctl".to_string(), + "request_key".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L600 + "@memlock".to_string(), + HashSet::from([ + "mlock".to_string(), + "mlock2".to_string(), + "mlockall".to_string(), + "munlock".to_string(), + "munlockall".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L610 + "@module".to_string(), + HashSet::from([ + "delete_module".to_string(), + "finit_module".to_string(), + "init_module".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L618 + "@mount".to_string(), + HashSet::from([ + "chroot".to_string(), + "fsconfig".to_string(), + "fsmount".to_string(), + "fsopen".to_string(), + "fspick".to_string(), + "mount".to_string(), + "mount_setattr".to_string(), + "move_mount".to_string(), + "open_tree".to_string(), + "pivot_root".to_string(), + "umount".to_string(), + "umount2".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L635 + "@network-io".to_string(), + HashSet::from([ + "accept".to_string(), + "accept4".to_string(), + "bind".to_string(), + "connect".to_string(), + "getpeername".to_string(), + "getsockname".to_string(), + "getsockopt".to_string(), + "listen".to_string(), + "recv".to_string(), + "recvfrom".to_string(), + "recvmmsg".to_string(), + "recvmmsg_time64".to_string(), + "recvmsg".to_string(), + "send".to_string(), + "sendmmsg".to_string(), + "sendmsg".to_string(), + "sendto".to_string(), + "setsockopt".to_string(), + "shutdown".to_string(), + "socket".to_string(), + "socketcall".to_string(), + "socketpair".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L662 + "@obsolete".to_string(), + HashSet::from([ + "_sysctl".to_string(), + "afs_syscall".to_string(), + "bdflush".to_string(), + "break".to_string(), + "create_module".to_string(), + "ftime".to_string(), + "get_kernel_syms".to_string(), + "getpmsg".to_string(), + "gtty".to_string(), + "idle".to_string(), + "lock".to_string(), + "mpx".to_string(), + "prof".to_string(), + "profil".to_string(), + "putpmsg".to_string(), + "query_module".to_string(), + "security".to_string(), + "sgetmask".to_string(), + "ssetmask".to_string(), + "stime".to_string(), + "stty".to_string(), + "sysfs".to_string(), + "tuxcall".to_string(), + "ulimit".to_string(), + "uselib".to_string(), + "ustat".to_string(), + "vserver".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L695 + "@pkey".to_string(), + HashSet::from([ + "pkey_alloc".to_string(), + "pkey_free".to_string(), + "pkey_mprotect".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L703 + "@privileged".to_string(), + HashSet::from([ + "_sysctl".to_string(), + "acct".to_string(), + "bpf".to_string(), + "capset".to_string(), + "chroot".to_string(), + "fanotify_init".to_string(), + "fanotify_mark".to_string(), + "nfsservctl".to_string(), + "open_by_handle_at".to_string(), + "pivot_root".to_string(), + "quotactl".to_string(), + "quotactl_fd".to_string(), + "setdomainname".to_string(), + "setfsuid".to_string(), + "setfsuid32".to_string(), + "setgroups".to_string(), + "setgroups32".to_string(), + "sethostname".to_string(), + "setresuid".to_string(), + "setresuid32".to_string(), + "setreuid".to_string(), + "setreuid32".to_string(), + "setuid".to_string(), + "setuid32".to_string(), + "vhangup".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L739 + "@process".to_string(), + HashSet::from([ + "capget".to_string(), + "clone".to_string(), + "clone3".to_string(), + "execveat".to_string(), + "fork".to_string(), + "getrusage".to_string(), + "kill".to_string(), + "pidfd_open".to_string(), + "pidfd_send_signal".to_string(), + "prctl".to_string(), + "rt_sigqueueinfo".to_string(), + "rt_tgsigqueueinfo".to_string(), + "setns".to_string(), + "swapcontext".to_string(), + "tgkill".to_string(), + "times".to_string(), + "tkill".to_string(), + "unshare".to_string(), + "vfork".to_string(), + "wait4".to_string(), + "waitid".to_string(), + "waitpid".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L769 + "@raw-io".to_string(), + HashSet::from([ + "ioperm".to_string(), + "iopl".to_string(), + "pciconfig_iobase".to_string(), + "pciconfig_read".to_string(), + "pciconfig_write".to_string(), + "s390_pci_mmio_read".to_string(), + "s390_pci_mmio_write".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L781 + "@reboot".to_string(), + HashSet::from([ + "kexec_file_load".to_string(), + "kexec_load".to_string(), + "reboot".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L789 + "@resources".to_string(), + HashSet::from([ + "ioprio_set".to_string(), + "mbind".to_string(), + "migrate_pages".to_string(), + "move_pages".to_string(), + "nice".to_string(), + "sched_setaffinity".to_string(), + "sched_setattr".to_string(), + "sched_setparam".to_string(), + "sched_setscheduler".to_string(), + "set_mempolicy".to_string(), + "set_mempolicy_home_node".to_string(), + "setpriority".to_string(), + "setrlimit".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L807 + "@sandbox".to_string(), + HashSet::from([ + "landlock_add_rule".to_string(), + "landlock_create_ruleset".to_string(), + "landlock_restrict_self".to_string(), + "seccomp".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L816 + "@setuid".to_string(), + HashSet::from([ + "setgid".to_string(), + "setgid32".to_string(), + "setgroups".to_string(), + "setgroups32".to_string(), + "setregid".to_string(), + "setregid32".to_string(), + "setresgid".to_string(), + "setresgid32".to_string(), + "setresuid".to_string(), + "setresuid32".to_string(), + "setreuid".to_string(), + "setreuid32".to_string(), + "setuid".to_string(), + "setuid32".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L835 + "@signal".to_string(), + HashSet::from([ + "rt_sigaction".to_string(), + "rt_sigpending".to_string(), + "rt_sigprocmask".to_string(), + "rt_sigsuspend".to_string(), + "rt_sigtimedwait".to_string(), + "rt_sigtimedwait_time64".to_string(), + "sigaction".to_string(), + "sigaltstack".to_string(), + "signal".to_string(), + "signalfd".to_string(), + "signalfd4".to_string(), + "sigpending".to_string(), + "sigprocmask".to_string(), + "sigsuspend".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L854 + "@swap".to_string(), + HashSet::from([ + "swapoff".to_string(), + "swapon".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L861 + "@sync".to_string(), + HashSet::from([ + "fdatasync".to_string(), + "fsync".to_string(), + "msync".to_string(), + "sync".to_string(), + "sync_file_range".to_string(), + "sync_file_range2".to_string(), + "syncfs".to_string(), + ]) + ), + ( + // https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L939 + "@timer".to_string(), + HashSet::from([ + "alarm".to_string(), + "getitimer".to_string(), + "setitimer".to_string(), + "timer_create".to_string(), + "timer_delete".to_string(), + "timer_getoverrun".to_string(), + "timer_gettime".to_string(), + "timer_gettime64".to_string(), + "timer_settime".to_string(), + "timer_settime64".to_string(), + "timerfd_create".to_string(), + "timerfd_gettime".to_string(), + "timerfd_gettime64".to_string(), + "timerfd_settime".to_string(), + "timerfd_settime64".to_string(), + "times".to_string(), + ]) + ), + ]); +} + +#[allow(clippy::vec_init_then_push)] +pub fn build_options( + systemd_version: &SystemdVersion, + kernel_version: &KernelVersion, +) -> Vec { + let mut options = Vec::new(); + + // + // Warning: options values must be ordered from less to most restrictive + // + + // TODO APPROXIMATION + // Some options implicitly force NoNewPrivileges=true which has some effects in itself, + // which we need to model + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectSystem= + let protect_system_yes_nowrite: Vec<_> = [ + "/usr/", "/boot/", "/efi/", "/lib/", "/lib64/", "/bin/", "/sbin/", + ] + .iter() + .map(|p| { + OptionValueEffect::DenyWrite(PathDescription::Base { + base: p.into(), + exceptions: vec![], + }) + }) + .collect(); + let mut protect_system_full_nowrite = protect_system_yes_nowrite.clone(); + protect_system_full_nowrite.push(OptionValueEffect::DenyWrite(PathDescription::Base { + base: "/etc/".into(), + exceptions: vec![], + })); + options.push(OptionDescription { + name: "ProtectSystem".to_string(), + possible_values: vec![ + OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::Multiple(protect_system_yes_nowrite)), + }, + OptionValueDescription { + value: OptionValue::String("full".to_string()), + desc: OptionEffect::Simple(OptionValueEffect::Multiple( + protect_system_full_nowrite, + )), + }, + OptionValueDescription { + value: OptionValue::String("strict".to_string()), + desc: OptionEffect::Simple(OptionValueEffect::DenyWrite(PathDescription::Base { + base: "/".into(), + exceptions: vec!["/dev/".into(), "/proc/".into(), "/sys/".into()], + })), + }, + ], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome= + let home_paths = ["/home/", "/root/", "/run/user/"]; + options.push(OptionDescription { + name: "ProtectHome".to_string(), + possible_values: vec![ + OptionValueDescription { + value: OptionValue::String("read-only".to_string()), + desc: OptionEffect::Simple(OptionValueEffect::Multiple( + home_paths + .iter() + .map(|p| { + OptionValueEffect::DenyWrite(PathDescription::Base { + base: p.into(), + exceptions: vec![], + }) + }) + .collect(), + )), + }, + OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::Multiple( + home_paths + .iter() + .map(|p| { + OptionValueEffect::Hide(PathDescription::Base { + base: p.into(), + exceptions: vec![], + }) + }) + .collect(), + )), + }, + OptionValueDescription { + value: OptionValue::String("tmpfs".to_string()), + desc: OptionEffect::Simple(OptionValueEffect::Multiple( + home_paths + .iter() + .map(|p| { + OptionValueEffect::Hide(PathDescription::Base { + base: p.into(), + exceptions: vec![], + }) + }) + .chain(home_paths.iter().map(|p| { + OptionValueEffect::DenyWrite(PathDescription::Base { + base: p.into(), + exceptions: vec![], + }) + })) + .collect(), + )), + }, + ], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp= + options.push(OptionDescription { + name: "PrivateTmp".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::Multiple(vec![ + OptionValueEffect::Hide(PathDescription::Base { + base: "/tmp/".into(), + exceptions: vec![], + }), + OptionValueEffect::Hide(PathDescription::Base { + base: "/var/tmp/".into(), + exceptions: vec![], + }), + ])), + }], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices= + options.push(OptionDescription { + name: "PrivateDevices".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::Multiple(vec![ + OptionValueEffect::Hide(PathDescription::Base { + base: "/dev/".into(), + // https://github.com/systemd/systemd/blob/v254/src/core/namespace.c#L912 + exceptions: [ + "null", + "zero", + "full", + "random", + "urandom", + "tty", + "pts/", + "ptmx", + "shm/", + "mqueue/", + "hugepages/", + "log", + ] + .iter() + .map(|p| PathBuf::from("/dev/").join(p)) + .collect(), + }), + OptionValueEffect::DenySyscall { + class: "@raw-io".to_string(), + }, + ])), + }], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables= + options.push(OptionDescription { + name: "ProtectKernelTunables".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::Multiple( + // https://github.com/systemd/systemd/blob/v254/src/core/namespace.c#L113 + [ + "acpi/", + "apm", + "asound/", + "bus/", + "fs/", + "irq/", + "latency_stats", + "mttr", + "scsi/", + "sys/", + "sysrq-trigger", + "timer_stats", + ] + .iter() + .map(|p| { + OptionValueEffect::DenyWrite(PathDescription::Base { + base: PathBuf::from("/proc/").join(p), + exceptions: vec![], + }) + }) + .chain(["kallsyms", "kcore"].iter().map(|p| { + OptionValueEffect::Hide(PathDescription::Base { + base: PathBuf::from("/proc/").join(p), + exceptions: vec![], + }) + })) + .chain( + // https://github.com/systemd/systemd/blob/v254/src/core/namespace.c#L130 + iter::once(OptionValueEffect::DenyWrite(PathDescription::Base { + base: "/sys/".into(), + exceptions: vec![], + })), + ) + .collect(), + )), + }], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules= + options.push(OptionDescription { + name: "ProtectKernelModules".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::Multiple(vec![ + // https://github.com/systemd/systemd/blob/v254/src/core/namespace.c#L140 + OptionValueEffect::Hide(PathDescription::Base { + base: "/lib/modules/".into(), + exceptions: vec![], + }), + OptionValueEffect::Hide(PathDescription::Base { + base: "/usr/lib/modules/".into(), + exceptions: vec![], + }), + OptionValueEffect::DenySyscall { + class: "@module".to_string(), + }, + ])), + }], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelLogs= + options.push(OptionDescription { + name: "ProtectKernelLogs".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::Multiple(vec![ + // https://github.com/systemd/systemd/blob/v254/src/core/namespace.c#L140 + OptionValueEffect::Hide(PathDescription::Base { + base: "/proc/kmsg".into(), + exceptions: vec![], + }), + OptionValueEffect::Hide(PathDescription::Base { + base: "/dev/kmsg".into(), + exceptions: vec![], + }), + ])), + }], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups= + options.push(OptionDescription { + name: "ProtectControlGroups".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::Boolean(true), + desc: OptionEffect::Simple(OptionValueEffect::DenyWrite(PathDescription::Base { + base: "/sys/fs/cgroup/".into(), + exceptions: vec![], + })), + }], + }); + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectProc= + // https://github.com/systemd/systemd/blob/v247/NEWS#L342 + // https://github.com/systemd/systemd/commit/4e39995371738b04d98d27b0d34ea8fe09ec9fab + // https://docs.kernel.org/filesystems/proc.html#mount-options + if (systemd_version >= &SystemdVersion::new(247, 0)) + && (kernel_version >= &KernelVersion::new(5, 8, 0)) + { + options.push(OptionDescription { + name: "ProtectProc".to_string(), + // Since we have no easy & reliable (race free) way to know which process belongs to + // which user, only support the most restrictive option + possible_values: vec![OptionValueDescription { + value: OptionValue::String("ptraceable".to_string()), + desc: OptionEffect::Simple(OptionValueEffect::Hide(PathDescription::Pattern( + regex::bytes::Regex::new("^/proc/[0-9]+(/|$)").unwrap(), + ))), + }], + }); + } + + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter= + let mut syscall_classes: Vec<_> = SYSCALL_CLASSES.keys().cloned().collect(); + syscall_classes.sort(); + options.push(OptionDescription { + name: "SystemCallFilter".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::DenyList(syscall_classes.clone()), + desc: OptionEffect::Cumulative( + syscall_classes + .into_iter() + .map(|class| OptionValueEffect::DenySyscall { class }) + .collect(), + ), + }], + }); + + log::debug!("{options:#?}"); + options +} diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs new file mode 100644 index 0000000..fabeab4 --- /dev/null +++ b/src/systemd/resolver.rs @@ -0,0 +1,204 @@ +//! Resolver code that finds options compatible with program actions + +use crate::summarize::ProgramAction; +use crate::systemd::options::{ + OptionDescription, OptionEffect, OptionValue, OptionValueEffect, OptionWithValue, + SYSCALL_CLASSES, +}; + +impl OptionValueEffect { + fn compatible(&self, action: &ProgramAction, prev_actions: &[ProgramAction]) -> bool { + match self { + OptionValueEffect::DenyWrite(ro_paths) => match action { + ProgramAction::Write(path_action) | ProgramAction::Create(path_action) => { + !ro_paths.matches(path_action) + } + _ => true, + }, + OptionValueEffect::Hide(hidden_paths) => { + if let ProgramAction::Read(path_action) = action { + !hidden_paths.matches(path_action) + || prev_actions.contains(&ProgramAction::Create(path_action.clone())) + } else { + true + } + } + OptionValueEffect::DenySyscall { class } => { + if let ProgramAction::Syscalls(syscalls) = action { + let denied = SYSCALL_CLASSES.get(class).unwrap(); + denied.intersection(syscalls).next().is_none() + } else { + true + } + } + OptionValueEffect::Multiple(effects) => { + effects.iter().all(|e| e.compatible(action, prev_actions)) + } + } + } +} + +pub fn actions_compatible(eff: &OptionValueEffect, actions: &[ProgramAction]) -> bool { + for i in 0..actions.len() { + if !eff.compatible(&actions[i], &actions[..i]) { + log::debug!( + "Option effect {:?} is incompatible with {:?}", + eff, + actions[i] + ); + return false; + } + } + true +} + +pub fn resolve( + opts: &Vec, + actions: &[ProgramAction], +) -> anyhow::Result> { + let mut candidates = Vec::new(); + for opt in opts { + // Options are in the less to most restrictive order, + // so for non cumulative options, iterate from the end + for opt_value_desc in opt.possible_values.iter().rev() { + match &opt_value_desc.desc { + OptionEffect::Simple(effect) => { + if actions_compatible(effect, actions) { + candidates.push(OptionWithValue { + name: opt.name.clone(), + value: opt_value_desc.value.clone(), + }); + break; + } + } + OptionEffect::Cumulative(effects) => { + let opt_values = if let OptionValue::DenyList(v) = &opt_value_desc.value { + v + } else { + unreachable!() + }; + debug_assert_eq!(opt_values.len(), effects.len()); + let mut compatible_opts = Vec::new(); + for (optv, opte) in opt_values.iter().zip(effects) { + if actions_compatible(opte, actions) { + compatible_opts.push(optv.clone()); + } + } + if !opt_values.is_empty() { + candidates.push(OptionWithValue { + name: opt.name.clone(), + value: OptionValue::DenyList(compatible_opts), + }); + } + } + } + } + } + Ok(candidates) +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::systemd::{build_options, KernelVersion, SystemdVersion}; + + fn test_options(names: &[&str]) -> Vec { + let sd_version = SystemdVersion::new(254, 0); + let kernel_version = KernelVersion::new(6, 4, 0); + build_options(&sd_version, &kernel_version) + .into_iter() + .filter(|o| names.contains(&o.name.as_str())) + .collect() + } + + #[test] + fn test_resolve_protect_system() { + let _ = simple_logger::SimpleLogger::new().init(); + + let opts = test_options(&["ProtectSystem"]); + + let actions = vec![]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectSystem=strict"); + + let actions = vec![ProgramAction::Write("/sys/whatever".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectSystem=strict"); + + let actions = vec![ProgramAction::Write("/var/cache/whatever".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectSystem=full"); + + let actions = vec![ProgramAction::Write("/etc/plop.conf".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectSystem=true"); + + let actions = vec![ProgramAction::Write("/usr/bin/false".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 0); + } + + #[test] + fn test_resolve_protect_home() { + let _ = simple_logger::SimpleLogger::new().init(); + + let opts = test_options(&["ProtectHome"]); + + let actions = vec![]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectHome=tmpfs"); + + let actions = vec![ProgramAction::Write("/home/user/data".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectHome=true"); + + let actions = vec![ProgramAction::Read("/home/user/data".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectHome=read-only"); + + let actions = vec![ + ProgramAction::Create("/home/user/data".into()), + ProgramAction::Read("/home/user/data".into()), + ]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "ProtectHome=true"); + } + + #[test] + fn test_resolve_private_tmp() { + let _ = simple_logger::SimpleLogger::new().init(); + + let opts = test_options(&["PrivateTmp"]); + + let actions = vec![]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "PrivateTmp=true"); + + let actions = vec![ProgramAction::Write("/tmp/data".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "PrivateTmp=true"); + + let actions = vec![ProgramAction::Read("/tmp/data".into())]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 0); + + let actions = vec![ + ProgramAction::Create("/tmp/data".into()), + ProgramAction::Read("/tmp/data".into()), + ]; + let candidates = resolve(&opts, &actions).unwrap(); + assert_eq!(candidates.len(), 1); + assert_eq!(format!("{}", candidates[0]), "PrivateTmp=true"); + } +} diff --git a/src/systemd/service.rs b/src/systemd/service.rs new file mode 100644 index 0000000..d7fea4e --- /dev/null +++ b/src/systemd/service.rs @@ -0,0 +1,303 @@ +//! Systemd service actions + +use std::env; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::path::PathBuf; +use std::process::{Command, Stdio}; + +use itertools::Itertools; + +use crate::systemd::{ + options::OptionWithValue, END_OPTION_OUTPUT_SNIPPET, START_OPTION_OUTPUT_SNIPPET, +}; + +pub struct Service { + name: String, + arg: Option, +} + +const PROFILING_FRAGMENT_NAME: &str = "profile"; +const HARDENING_FRAGMENT_NAME: &str = "harden"; + +impl Service { + pub fn new(unit: &str) -> Self { + if let Some((name, arg)) = unit.split_once('@') { + Self { + name: name.to_string(), + arg: Some(arg.to_string()), + } + } else { + Self { + name: unit.to_string(), + arg: None, + } + } + } + + fn unit_name(&self) -> String { + format!( + "{}{}.service", + &self.name, + if let Some(arg) = self.arg.as_ref() { + format!("@{arg}") + } else { + "".to_string() + } + ) + } + + pub fn add_profile_fragment(&self) -> anyhow::Result<()> { + // Check first if our fragment does not yet exist + let fragment_path = self.fragment_path(PROFILING_FRAGMENT_NAME, false); + anyhow::ensure!( + !fragment_path.is_file(), + "Fragment config already exists at {fragment_path:?}" + ); + let harden_fragment_path = self.fragment_path(HARDENING_FRAGMENT_NAME, true); + anyhow::ensure!( + !harden_fragment_path.is_file(), + "Hardening config already exists at {harden_fragment_path:?} and may conflict with profiling" + ); + + // TODO check service is not of type oneshot with several ExecStart ? + + // Read current service startup command + let config_paths = self.config_paths()?; + log::info!("Located unit config file(s): {config_paths:?}"); + let cmd = Self::config_val("ExecStart", &config_paths)? + .ok_or_else(|| anyhow::anyhow!("Unable to get service startup command"))?; + log::info!("Startup command: {cmd}"); + + // Write new fragment + fs::create_dir_all(fragment_path.parent().unwrap())?; + let mut fragment_file = BufWriter::new(File::create(&fragment_path)?); + writeln!( + fragment_file, + "# This file has been autogenerated by {}", + env!("CARGO_PKG_NAME") + )?; + writeln!(fragment_file, "[Service]")?; + // writeln!(fragment_file, "AmbientCapabilities=CAP_SYS_PTRACE")?; + // needed because strace becomes the main process + writeln!(fragment_file, "NotifyAccess=all")?; + writeln!(fragment_file, "Environment=RUST_BACKTRACE=1")?; + if Self::config_val("SystemCallFilter", &config_paths)?.is_some() { + // Allow ptracing, only if a syscall filter is already in place, otherwise it becomes a whitelist + writeln!(fragment_file, "SystemCallFilter=@debug")?; + } + // strace may slow down enough to risk reaching some service timeouts + writeln!(fragment_file, "TimeoutStartSec=infinity")?; + writeln!(fragment_file, "ExecStart=")?; + writeln!( + fragment_file, + "ExecStart={} run -- {}", + env::current_exe()? + .to_str() + .ok_or_else(|| anyhow::anyhow!("Unable to decode current executable path"))?, + cmd + )?; + + log::info!("Config fragment written in {fragment_path:?}"); + Ok(()) + } + + pub fn remove_profile_fragment(&self) -> anyhow::Result<()> { + let fragment_path = self.fragment_path(PROFILING_FRAGMENT_NAME, false); + fs::remove_file(&fragment_path)?; + log::info!("{fragment_path:?} removed"); + // let mut parent_dir = fragment_path; + // while let Some(parent_dir) = parent_dir.parent() { + // if fs::remove_dir(parent_dir).is_err() { + // // Likely directory not empty + // break; + // } + // log::info!("{parent_dir:?} removed"); + // } + Ok(()) + } + + pub fn remove_hardening_fragment(&self) -> anyhow::Result<()> { + let fragment_path = self.fragment_path(HARDENING_FRAGMENT_NAME, true); + fs::remove_file(&fragment_path)?; + log::info!("{fragment_path:?} removed"); + Ok(()) + } + + pub fn add_hardening_fragment(&self, opts: Vec) -> anyhow::Result<()> { + let fragment_path = self.fragment_path(HARDENING_FRAGMENT_NAME, true); + fs::create_dir_all(fragment_path.parent().unwrap())?; + + let mut fragment_file = BufWriter::new(File::create(&fragment_path)?); + writeln!( + fragment_file, + "# This file has been autogenerated by {}", + env!("CARGO_PKG_NAME") + )?; + writeln!(fragment_file, "[Service]")?; + for opt in opts { + writeln!(fragment_file, "{opt}")?; + } + + log::info!("Config fragment written in {fragment_path:?}"); + Ok(()) + } + + pub fn reload_unit_config(&self) -> anyhow::Result<()> { + let status = Command::new("systemctl").arg("daemon-reload").status()?; + if !status.success() { + anyhow::bail!("systemctl failed: {status}"); + } + Ok(()) + } + + pub fn action(&self, verb: &str) -> anyhow::Result<()> { + log::info!("{} {}", verb, &self.unit_name()); + let status = Command::new("systemctl") + .args([verb, &self.unit_name()]) + .status()?; + if !status.success() { + anyhow::bail!("systemctl failed: {status}"); + } + Ok(()) + } + + pub fn profiling_result(&self) -> anyhow::Result> { + // Start journalctl process + let mut child = Command::new("journalctl") + .args([ + "-r", + "-o", + "cat", + "--output-fields=MESSAGE", + "--no-tail", + "-u", + &self.unit_name(), + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .env("LANG", "C") + .spawn()?; + + // Parse its output + let reader = BufReader::new(child.stdout.take().unwrap()); + let snippet_lines: Vec<_> = reader + .lines() + // Stream lines but bubble up errors + .skip_while(|r| { + r.as_ref() + .map(|l| l != END_OPTION_OUTPUT_SNIPPET) + .unwrap_or(false) + }) + .take_while_inclusive(|r| { + r.as_ref() + .map(|l| l != START_OPTION_OUTPUT_SNIPPET) + .unwrap_or(true) + }) + .collect::>()?; + if (snippet_lines.len() < 2) + || (snippet_lines.last().unwrap() != START_OPTION_OUTPUT_SNIPPET) + { + anyhow::bail!("Unable to get profiling result snippet"); + } + // The output with '-r' flag is in reverse chronological order + // (to get the end as fast as possible), so reverse it, after we have + // removed marker lines + let opts = snippet_lines[1..snippet_lines.len() - 1] + .iter() + .rev() + .map(|l| l.parse::()) + .collect::>()?; + + // Stop journalctl + child.kill()?; + child.wait()?; + + Ok(opts) + } + + fn config_val(key: &str, config_paths: &[PathBuf]) -> anyhow::Result> { + for config_path in config_paths.iter().rev() { + let config_file = BufReader::new(File::open(config_path)?); + if let Some(val) = config_file + .lines() + .filter(|l| { + l.as_ref() + .map(|l| l.starts_with(&format!("{key}="))) + .unwrap_or(true) + }) + .last() // TODO handle multiline options? + .map(|l| l.map(|l| l.split_once('=').unwrap().1.trim().to_string())) + { + let val = val?; + return Ok(Some(val)); + } + } + Ok(None) + } + + fn config_paths(&self) -> anyhow::Result> { + let output = Command::new("systemctl") + .args(["status", "-n", "0", &self.unit_name()]) + .env("LANG", "C") + .output()?; + let mut paths = Vec::new(); + let mut drop_in_dir = None; + for line in output.stdout.lines() { + let line = line?; + let line = line.trim_start(); + if line.starts_with("Loaded:") { + // Main unit file + anyhow::ensure!(paths.is_empty()); + let path = line + .split_once('(') + .ok_or_else(|| anyhow::anyhow!("Failed to locate main unit file"))? + .1 + .split_once(';') + .ok_or_else(|| anyhow::anyhow!("Failed to locate main unit file"))? + .0; + paths.push(PathBuf::from(path)); + } else if line.starts_with("Drop-In:") { + // Drop in base dir + anyhow::ensure!(paths.len() == 1); + anyhow::ensure!(drop_in_dir.is_none()); + let dir = line + .split_once(':') + .ok_or_else(|| anyhow::anyhow!("Failed to locate unit config fragment dir"))? + .1 + .trim_start(); + drop_in_dir = Some(PathBuf::from(dir)); + } else if let Some(dir) = drop_in_dir.as_ref() { + if line.contains(':') { + // Not a path, next key: val line + break; + } else if line.starts_with('/') { + // New base dir + drop_in_dir = Some(PathBuf::from(line)); + } else { + for filename in line.trim().chars().skip(2).collect::().split(", ") { + let path = dir.join(filename); + paths.push(path); + } + } + } + } + Ok(paths) + } + + fn fragment_path(&self, name: &str, persistent: bool) -> PathBuf { + [ + if persistent { "/etc" } else { "/run" }, + "systemd/system/", + &format!( + "{}{}.service.d", + self.name, + if self.arg.is_some() { "@" } else { "" } + ), + &format!("zz_{}-{}.conf", env!("CARGO_PKG_NAME"), name), + ] + .iter() + .collect() + } +} diff --git a/src/systemd/version.rs b/src/systemd/version.rs new file mode 100644 index 0000000..1619b36 --- /dev/null +++ b/src/systemd/version.rs @@ -0,0 +1,92 @@ +//! Systemd & kernel version + +use std::fmt; +use std::process::Command; +use std::str; + +#[derive(Ord, PartialOrd, Eq, PartialEq)] +pub struct SystemdVersion { + pub major: u16, + pub minor: u16, +} + +impl SystemdVersion { + pub fn new(major: u16, minor: u16) -> Self { + Self { major, minor } + } + + pub fn local_system() -> anyhow::Result { + let output = Command::new("systemctl").arg("--version").output()?; + if !output.status.success() { + anyhow::bail!("systemctl invocation failed with code {:?}", output.status); + } + let (major, rest) = str::from_utf8(&output.stdout)? + .split_once('(') + .ok_or_else(|| anyhow::anyhow!("Unable to get systemd major version"))? + .1 + .split_once('.') + .ok_or_else(|| anyhow::anyhow!("Unable to get systemd minor version"))?; + let minor = rest + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::(); + Ok(Self { + major: major.parse()?, + minor: minor.parse()?, + }) + } +} + +impl fmt::Display for SystemdVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +#[derive(Ord, PartialOrd, Eq, PartialEq)] +pub struct KernelVersion { + major: u16, + minor: u16, + release: u16, +} + +impl KernelVersion { + pub fn new(major: u16, minor: u16, release: u16) -> Self { + Self { + major, + minor, + release, + } + } + + pub fn local_system() -> anyhow::Result { + let output = Command::new("uname").arg("-r").output()?; + if !output.status.success() { + anyhow::bail!("uname invocation failed with code {:?}", output.status); + } + let tokens: Vec<_> = str::from_utf8(&output.stdout)?.splitn(3, '.').collect(); + let release = tokens + .get(2) + .ok_or_else(|| anyhow::anyhow!("Unable to get kernel release version"))? + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect::(); + Ok(Self { + major: tokens + .first() + .ok_or_else(|| anyhow::anyhow!("Unable to get kernel major version"))? + .parse()?, + minor: tokens + .get(1) + .ok_or_else(|| anyhow::anyhow!("Unable to get kernel minor version"))? + .parse()?, + release: release.parse()?, + }) + } +} + +impl fmt::Display for KernelVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.release) + } +} diff --git a/tests/cl.rs b/tests/cl.rs new file mode 100644 index 0000000..0ed4c78 --- /dev/null +++ b/tests/cl.rs @@ -0,0 +1,167 @@ +//! Command line tests + +use assert_cmd::{assert::OutputAssertExt, Command}; +use predicates::prelude::*; + +// +// Important: these tests assume they are run from a dir under /home, so we don't test for ProtectHome=xxx +// option values, because they are affected by that. +// See test_resolve_protect_home for unit test covering that option. +// + +#[test] +fn run_true() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "true"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1)); +} + +#[test] +fn run_write_dev_null() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "sh", "-c", ": > /dev/null"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @swap @sync @timer\n").count(1)); +} + +#[test] +fn run_ls_dev() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "ls", "/dev"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").not()) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1)); +} + +#[test] +fn run_ls_proc() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "ls", "/proc/1/"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").not()) + .stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1)); +} + +#[test] +fn run_read_kallsyms() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "head", "/proc/kallsyms"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").not()) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1)); +} + +#[test] +fn run_ls_modules() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "ls", "/usr/lib/modules/"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").not()) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1)); +} + +#[test] +#[ignore] // needs to be run as root +fn run_dmesg() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "dmesg"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").not()) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").not()) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1)); +} + +#[test] +fn run_systemctl() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "systemctl", "--user"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("ProtectHome=read-only\n").count(1)) + .stdout(predicate::str::contains("PrivateTmp=true\n").count(1)) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicates::boolean::OrPredicate::new( + predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @ipc @keyring @memlock @module @mount @obsolete @pkey @privileged @raw-io @reboot @resources @sandbox @setuid @swap @sync @timer\n").count(1), + predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @obsolete @pkey @privileged @raw-io @reboot @resources @sandbox @setuid @swap @sync @timer\n").count(1), + )); +}