├── .github ├── dependabot.yaml ├── version_check.py └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── images ├── somo-compact-example.png ├── somo-example.png ├── somo-kill-example.png └── somo-logo.png ├── nix ├── flake.lock ├── flake.nix └── package.nix └── src ├── cli.rs ├── connections ├── common.rs ├── linux.rs ├── macos.rs └── mod.rs ├── macros.rs ├── main.rs ├── schemas.rs ├── table.rs └── utils.rs /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#package-ecosystem- 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: cargo 6 | directory: / 7 | schedule: 8 | interval: weekly 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/version_check.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import re 4 | from enum import Enum 5 | 6 | 7 | class BumpType(Enum): 8 | MAJOR = "major" 9 | MINOR = "minor" 10 | PATCH = "patch" 11 | 12 | 13 | def is_valid_semver(version: str) -> bool: 14 | semver_regex = r"^\d+\.\d+\.\d+quot; 15 | return bool(re.match(semver_regex, version)) 16 | 17 | 18 | def raise_semver_exception(): 19 | raise ValueError("Invalid version - must follow SEMVER convention (major.minor.patch)!") 20 | 21 | 22 | def validate_semver_bump(previous_version: str, next_version: str) -> BumpType: 23 | if not (is_valid_semver(previous_version) and is_valid_semver(next_version)): 24 | raise_semver_exception() 25 | 26 | if previous_version == next_version: 27 | raise ValueError("Version was not bumped!") 28 | 29 | print(f"Comparing next version {next_version} with previous version {previous_version}.") 30 | 31 | previous_version = list(map(int, previous_version.split("."))) 32 | next_version = list(map(int, next_version.split("."))) 33 | 34 | semver_parts = [BumpType.MAJOR, BumpType.MINOR, BumpType.PATCH] 35 | for idx in range(len(semver_parts)): 36 | if next_version[idx] != previous_version[idx]: 37 | correct_bump = next_version[idx] - 1 == previous_version[idx] 38 | lower_levels_reset = all(next_version[jdx] == 0 for jdx in range(idx + 1, len(semver_parts))) 39 | 40 | if correct_bump and lower_levels_reset: 41 | print(f"{semver_parts[idx].value} version bump.") 42 | return semver_parts[idx] 43 | 44 | raise_semver_exception() 45 | 46 | 47 | if __name__ == "__main__": 48 | previous_version = sys.argv[1].removeprefix("v") 49 | next_version = sys.argv[2].removeprefix("v") 50 | 51 | bump_type = validate_semver_bump(previous_version, next_version) 52 | with open(os.getenv("GITHUB_OUTPUT"), "a") as output: 53 | output.write(f"bump_type={bump_type.value}\n") -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**/README.md" 7 | branches: 8 | - master 9 | pull_request: 10 | branches: ["**"] 11 | workflow_dispatch: 12 | workflow_call: 13 | 14 | env: 15 | CARGO_TERM_COLOR: always 16 | RUST_BACKTRACE: 1 17 | 18 | jobs: 19 | build: 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, macos-latest] 24 | 25 | steps: 26 | - name: Setup | Rust 27 | uses: dtolnay/rust-toolchain@stable 28 | with: 29 | components: rustfmt, clippy, rust-src 30 | 31 | - name: Setup | Checkout 32 | uses: actions/checkout@v4 33 | 34 | - name: Build | Compile 35 | run: cargo build 36 | 37 | 38 | check-format: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Setup | Rust 42 | uses: dtolnay/rust-toolchain@stable 43 | 44 | - name: Setup | Checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: Check | Fmt Check 48 | run: cargo fmt -- --check 49 | 50 | 51 | check-clippy: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - name: Setup | Rust 55 | uses: dtolnay/rust-toolchain@stable 56 | 57 | - name: Setup | Checkout 58 | uses: actions/checkout@v4 59 | 60 | - name: Check | Clippy 61 | run: cargo clippy --no-deps -- -Dwarnings 62 | 63 | 64 | test: 65 | runs-on: ${{ matrix.os }} 66 | strategy: 67 | matrix: 68 | os: [ubuntu-latest, macos-latest] 69 | steps: 70 | - name: Setup | Rust 71 | uses: dtolnay/rust-toolchain@stable 72 | 73 | - name: Setup | Checkout 74 | uses: actions/checkout@v4 75 | 76 | - name: Test | Run 77 | run: cargo test 78 | 79 | nix: 80 | runs-on: ubuntu-latest 81 | steps: 82 | - uses: actions/checkout@v4 83 | - uses: DeterminateSystems/nix-installer-action@90bb610b90bf290cad97484ba341453bd1cbefea # v19 84 | - run: nix run 'path:.?dir=nix' 85 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | release_type: 7 | description: "Type of release" 8 | required: true 9 | type: choice 10 | options: 11 | - patch 12 | - minor 13 | - major 14 | 15 | jobs: 16 | ci: 17 | uses: ./.github/workflows/ci.yml 18 | 19 | version-check: 20 | needs: ci 21 | runs-on: ubuntu-latest 22 | 23 | outputs: 24 | bump_type: ${{ steps.check_version.outputs.bump_type }} 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - name: Get new version 33 | id: new_version 34 | run: echo ::set-output name=version::$(grep -Po '^version = \"\K[^\"]+' Cargo.toml) 35 | 36 | - name: Get previous version 37 | id: previous_version 38 | run: echo ::set-output name=version::$(git describe --tags --abbrev=0 || echo '0.0.0') 39 | 40 | - name: Check version 41 | id: check_version 42 | run: | 43 | python .github/version_check.py ${{ steps.previous_version.outputs.version }} ${{ steps.new_version.outputs.version }} 44 | echo "${{ github.event_name }} ${{ github.ref }}" 45 | 46 | - name: Validate selected release type against version bump 47 | run: | 48 | echo "Expected release type: ${{ github.event.inputs.release_type }}" 49 | echo "Actual release type: ${{ steps.check_version.outputs.bump_type }}" 50 | if [ "${{ github.event.inputs.release_type }}" != "${{ steps.check_version.outputs.bump_type }}" ]; then 51 | echo "Mismatch between selected release type and actual version bump. Aborting release." 52 | exit 1 53 | fi 54 | 55 | build-and-release: 56 | needs: version-check 57 | 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Checkout code 61 | uses: actions/checkout@v4 62 | 63 | - name: Extract version from Cargo.toml 64 | id: version 65 | run: echo "version=$(grep -Po '^version = \"\K[^\"]+' Cargo.toml)" >> "$GITHUB_OUTPUT" 66 | 67 | - name: Extract changelog entry 68 | id: changelog 69 | run: | 70 | version=${{ steps.version.outputs.version }} 71 | content=$(awk -v ver="$version" ' 72 | $0 ~ "^## \\["ver"\\]" { found = 1; print; next } 73 | found && /^---$/ { exit } 74 | found { print } 75 | ' CHANGELOG.md) 76 | 77 | echo "changelog<<EOF" >> $GITHUB_OUTPUT 78 | echo "$content" >> $GITHUB_OUTPUT 79 | echo "EOF" >> $GITHUB_OUTPUT 80 | 81 | - name: Build and package 82 | run: | 83 | cargo build --release 84 | cargo install cargo-deb 85 | cargo deb --output target/debian/somo-${{ steps.version.outputs.version }}.deb 86 | 87 | - name: Upload Artifact 88 | uses: actions/upload-artifact@v4 89 | with: 90 | name: somo-${{ steps.version.outputs.version }}-${{ github.run_number }}-${{ github.sha }}.deb 91 | path: target/debian/somo-${{ steps.version.outputs.version }}.deb 92 | 93 | - name: Publish to Crates.io 94 | run: cargo publish 95 | env: 96 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 97 | 98 | - name: Create Release 99 | uses: actions/create-release@v1 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | with: 103 | tag_name: v${{ steps.version.outputs.version }} 104 | release_name: 🎉 Somo Release ${{ steps.version.outputs.version }} 105 | body: ${{ steps.changelog.outputs.changelog }} 106 | draft: false 107 | prerelease: false 108 | id: create_release 109 | 110 | - name: Upload Release Asset 111 | uses: actions/upload-release-asset@v1 112 | env: 113 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 114 | with: 115 | upload_url: ${{ steps.create_release.outputs.upload_url }} 116 | asset_path: target/debian/somo-${{ steps.version.outputs.version }}.deb 117 | asset_name: somo-${{ steps.version.outputs.version }}-${{ github.run_number }}-${{ github.sha }}.deb 118 | asset_content_type: application/x-debian-package 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.idea 3 | 4 | # Nix 5 | /result 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.0] - 09.07.2025 4 | - **add** macOS support, by @belingud 5 | - **add** sorting by column, by @gerelef 6 | - **fix** panicking when being piped, by @gerelef 7 | - **add** compact table view, by @theopfr 8 | - **add** format and clippy check to CI, by @jerry1098 9 | - **update** CLI arguments with `--tcp`, `--udp` flags, by @celeo 10 | - **add** shell completions, by @polponline 11 | - **add** Nix packaging support, by @kachick 12 | - **update** kill functionality to use SIGTERM, by @rongyi 13 | - **add** JSON and custom formattable output, by @aptypp @gerelef 14 | - **fix** typos in README, by @cma5 @robinhutty 15 | - **add** MIT license, by @theopfr 16 | 17 | --- 18 | 19 | ## [1.0.0] - 04.06.2025 20 | - **update** flags, by @theopfr 21 | - move ``--local-port`` to ``--port`` 22 | - move ``--port`` to ``--remote-port`` 23 | - add ``--listen`` 24 | - **remove** abuseipdb scanning, by @theopfr 25 | - **add** logo, by @theopfr 26 | - **add** tests, by @theopfr 27 | 28 | --- -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.19" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.11" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.7" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.1.3" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 70 | dependencies = [ 71 | "windows-sys 0.59.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.9" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 79 | dependencies = [ 80 | "anstyle", 81 | "once_cell_polyfill", 82 | "windows-sys 0.59.0", 83 | ] 84 | 85 | [[package]] 86 | name = "anyhow" 87 | version = "1.0.98" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 90 | 91 | [[package]] 92 | name = "autocfg" 93 | version = "1.5.0" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 96 | 97 | [[package]] 98 | name = "bindgen" 99 | version = "0.70.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" 102 | dependencies = [ 103 | "bitflags 2.9.1", 104 | "cexpr", 105 | "clang-sys", 106 | "itertools", 107 | "proc-macro2", 108 | "quote", 109 | "regex", 110 | "rustc-hash 1.1.0", 111 | "shlex", 112 | "syn 2.0.104", 113 | ] 114 | 115 | [[package]] 116 | name = "bindgen" 117 | version = "0.71.1" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" 120 | dependencies = [ 121 | "bitflags 2.9.1", 122 | "cexpr", 123 | "clang-sys", 124 | "itertools", 125 | "log", 126 | "prettyplease", 127 | "proc-macro2", 128 | "quote", 129 | "regex", 130 | "rustc-hash 2.1.1", 131 | "shlex", 132 | "syn 2.0.104", 133 | ] 134 | 135 | [[package]] 136 | name = "bitflags" 137 | version = "1.3.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 140 | 141 | [[package]] 142 | name = "bitflags" 143 | version = "2.9.1" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 146 | 147 | [[package]] 148 | name = "block-buffer" 149 | version = "0.10.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 152 | dependencies = [ 153 | "generic-array", 154 | ] 155 | 156 | [[package]] 157 | name = "bumpalo" 158 | version = "3.19.0" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 161 | 162 | [[package]] 163 | name = "byteorder" 164 | version = "1.5.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 167 | 168 | [[package]] 169 | name = "bytes" 170 | version = "1.10.1" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 173 | 174 | [[package]] 175 | name = "cc" 176 | version = "1.2.29" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" 179 | dependencies = [ 180 | "shlex", 181 | ] 182 | 183 | [[package]] 184 | name = "cexpr" 185 | version = "0.6.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 188 | dependencies = [ 189 | "nom", 190 | ] 191 | 192 | [[package]] 193 | name = "cfg-if" 194 | version = "1.0.1" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 197 | 198 | [[package]] 199 | name = "cfg_aliases" 200 | version = "0.2.1" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 203 | 204 | [[package]] 205 | name = "chrono" 206 | version = "0.4.41" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 209 | dependencies = [ 210 | "android-tzdata", 211 | "iana-time-zone", 212 | "num-traits", 213 | "windows-link", 214 | ] 215 | 216 | [[package]] 217 | name = "clang-sys" 218 | version = "1.8.1" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 221 | dependencies = [ 222 | "glob", 223 | "libc", 224 | "libloading", 225 | ] 226 | 227 | [[package]] 228 | name = "clap" 229 | version = "4.5.41" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" 232 | dependencies = [ 233 | "clap_builder", 234 | "clap_derive", 235 | ] 236 | 237 | [[package]] 238 | name = "clap_builder" 239 | version = "4.5.41" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" 242 | dependencies = [ 243 | "anstream", 244 | "anstyle", 245 | "clap_lex", 246 | "strsim", 247 | ] 248 | 249 | [[package]] 250 | name = "clap_complete" 251 | version = "4.5.55" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a" 254 | dependencies = [ 255 | "clap", 256 | ] 257 | 258 | [[package]] 259 | name = "clap_derive" 260 | version = "4.5.41" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" 263 | dependencies = [ 264 | "heck", 265 | "proc-macro2", 266 | "quote", 267 | "syn 2.0.104", 268 | ] 269 | 270 | [[package]] 271 | name = "clap_lex" 272 | version = "0.7.5" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 275 | 276 | [[package]] 277 | name = "colorchoice" 278 | version = "1.0.4" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 281 | 282 | [[package]] 283 | name = "convert_case" 284 | version = "0.7.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" 287 | dependencies = [ 288 | "unicode-segmentation", 289 | ] 290 | 291 | [[package]] 292 | name = "coolor" 293 | version = "1.1.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3" 296 | dependencies = [ 297 | "crossterm 0.29.0", 298 | ] 299 | 300 | [[package]] 301 | name = "core-foundation-sys" 302 | version = "0.8.7" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 305 | 306 | [[package]] 307 | name = "cpufeatures" 308 | version = "0.2.17" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 311 | dependencies = [ 312 | "libc", 313 | ] 314 | 315 | [[package]] 316 | name = "crc32fast" 317 | version = "1.5.0" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 320 | dependencies = [ 321 | "cfg-if", 322 | ] 323 | 324 | [[package]] 325 | name = "crokey" 326 | version = "1.2.0" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "5282b45c96c5978c8723ea83385cb9a488b64b7d175733f48d07bf9da514a863" 329 | dependencies = [ 330 | "crokey-proc_macros", 331 | "crossterm 0.29.0", 332 | "once_cell", 333 | "serde", 334 | "strict", 335 | ] 336 | 337 | [[package]] 338 | name = "crokey-proc_macros" 339 | version = "1.2.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "2ea0218d3fedf0797fa55676f1964ef5d27103d41ed0281b4bbd2a6e6c3d8d28" 342 | dependencies = [ 343 | "crossterm 0.29.0", 344 | "proc-macro2", 345 | "quote", 346 | "strict", 347 | "syn 2.0.104", 348 | ] 349 | 350 | [[package]] 351 | name = "crossbeam" 352 | version = "0.8.4" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 355 | dependencies = [ 356 | "crossbeam-channel", 357 | "crossbeam-deque", 358 | "crossbeam-epoch", 359 | "crossbeam-queue", 360 | "crossbeam-utils", 361 | ] 362 | 363 | [[package]] 364 | name = "crossbeam-channel" 365 | version = "0.5.15" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 368 | dependencies = [ 369 | "crossbeam-utils", 370 | ] 371 | 372 | [[package]] 373 | name = "crossbeam-deque" 374 | version = "0.8.6" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 377 | dependencies = [ 378 | "crossbeam-epoch", 379 | "crossbeam-utils", 380 | ] 381 | 382 | [[package]] 383 | name = "crossbeam-epoch" 384 | version = "0.9.18" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 387 | dependencies = [ 388 | "crossbeam-utils", 389 | ] 390 | 391 | [[package]] 392 | name = "crossbeam-queue" 393 | version = "0.3.12" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 396 | dependencies = [ 397 | "crossbeam-utils", 398 | ] 399 | 400 | [[package]] 401 | name = "crossbeam-utils" 402 | version = "0.8.21" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 405 | 406 | [[package]] 407 | name = "crossterm" 408 | version = "0.25.0" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" 411 | dependencies = [ 412 | "bitflags 1.3.2", 413 | "crossterm_winapi", 414 | "libc", 415 | "mio 0.8.11", 416 | "parking_lot", 417 | "signal-hook", 418 | "signal-hook-mio", 419 | "winapi", 420 | ] 421 | 422 | [[package]] 423 | name = "crossterm" 424 | version = "0.29.0" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" 427 | dependencies = [ 428 | "bitflags 2.9.1", 429 | "crossterm_winapi", 430 | "derive_more", 431 | "document-features", 432 | "mio 1.0.4", 433 | "parking_lot", 434 | "rustix 1.0.8", 435 | "signal-hook", 436 | "signal-hook-mio", 437 | "winapi", 438 | ] 439 | 440 | [[package]] 441 | name = "crossterm_winapi" 442 | version = "0.9.1" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 445 | dependencies = [ 446 | "winapi", 447 | ] 448 | 449 | [[package]] 450 | name = "crypto-common" 451 | version = "0.1.6" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 454 | dependencies = [ 455 | "generic-array", 456 | "typenum", 457 | ] 458 | 459 | [[package]] 460 | name = "darling" 461 | version = "0.20.11" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 464 | dependencies = [ 465 | "darling_core", 466 | "darling_macro", 467 | ] 468 | 469 | [[package]] 470 | name = "darling_core" 471 | version = "0.20.11" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 474 | dependencies = [ 475 | "fnv", 476 | "ident_case", 477 | "proc-macro2", 478 | "quote", 479 | "strsim", 480 | "syn 2.0.104", 481 | ] 482 | 483 | [[package]] 484 | name = "darling_macro" 485 | version = "0.20.11" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 488 | dependencies = [ 489 | "darling_core", 490 | "quote", 491 | "syn 2.0.104", 492 | ] 493 | 494 | [[package]] 495 | name = "derive_builder" 496 | version = "0.20.2" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 499 | dependencies = [ 500 | "derive_builder_macro", 501 | ] 502 | 503 | [[package]] 504 | name = "derive_builder_core" 505 | version = "0.20.2" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 508 | dependencies = [ 509 | "darling", 510 | "proc-macro2", 511 | "quote", 512 | "syn 2.0.104", 513 | ] 514 | 515 | [[package]] 516 | name = "derive_builder_macro" 517 | version = "0.20.2" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 520 | dependencies = [ 521 | "derive_builder_core", 522 | "syn 2.0.104", 523 | ] 524 | 525 | [[package]] 526 | name = "derive_more" 527 | version = "2.0.1" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 530 | dependencies = [ 531 | "derive_more-impl", 532 | ] 533 | 534 | [[package]] 535 | name = "derive_more-impl" 536 | version = "2.0.1" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 539 | dependencies = [ 540 | "convert_case", 541 | "proc-macro2", 542 | "quote", 543 | "syn 2.0.104", 544 | ] 545 | 546 | [[package]] 547 | name = "digest" 548 | version = "0.10.7" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 551 | dependencies = [ 552 | "block-buffer", 553 | "crypto-common", 554 | ] 555 | 556 | [[package]] 557 | name = "document-features" 558 | version = "0.2.11" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" 561 | dependencies = [ 562 | "litrs", 563 | ] 564 | 565 | [[package]] 566 | name = "dyn-clone" 567 | version = "1.0.19" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" 570 | 571 | [[package]] 572 | name = "either" 573 | version = "1.15.0" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 576 | 577 | [[package]] 578 | name = "errno" 579 | version = "0.3.13" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 582 | dependencies = [ 583 | "libc", 584 | "windows-sys 0.60.2", 585 | ] 586 | 587 | [[package]] 588 | name = "flate2" 589 | version = "1.1.2" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 592 | dependencies = [ 593 | "crc32fast", 594 | "miniz_oxide", 595 | ] 596 | 597 | [[package]] 598 | name = "fnv" 599 | version = "1.0.7" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 602 | 603 | [[package]] 604 | name = "fuzzy-matcher" 605 | version = "0.3.7" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" 608 | dependencies = [ 609 | "thread_local", 610 | ] 611 | 612 | [[package]] 613 | name = "fxhash" 614 | version = "0.2.1" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 617 | dependencies = [ 618 | "byteorder", 619 | ] 620 | 621 | [[package]] 622 | name = "generic-array" 623 | version = "0.14.7" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 626 | dependencies = [ 627 | "typenum", 628 | "version_check", 629 | ] 630 | 631 | [[package]] 632 | name = "glob" 633 | version = "0.3.2" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 636 | 637 | [[package]] 638 | name = "handlebars" 639 | version = "6.3.2" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" 642 | dependencies = [ 643 | "derive_builder", 644 | "log", 645 | "num-order", 646 | "pest", 647 | "pest_derive", 648 | "serde", 649 | "serde_json", 650 | "thiserror 2.0.12", 651 | ] 652 | 653 | [[package]] 654 | name = "heck" 655 | version = "0.5.0" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 658 | 659 | [[package]] 660 | name = "hex" 661 | version = "0.4.3" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 664 | 665 | [[package]] 666 | name = "iana-time-zone" 667 | version = "0.1.63" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 670 | dependencies = [ 671 | "android_system_properties", 672 | "core-foundation-sys", 673 | "iana-time-zone-haiku", 674 | "js-sys", 675 | "log", 676 | "wasm-bindgen", 677 | "windows-core", 678 | ] 679 | 680 | [[package]] 681 | name = "iana-time-zone-haiku" 682 | version = "0.1.2" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 685 | dependencies = [ 686 | "cc", 687 | ] 688 | 689 | [[package]] 690 | name = "ident_case" 691 | version = "1.0.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 694 | 695 | [[package]] 696 | name = "inquire" 697 | version = "0.7.5" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" 700 | dependencies = [ 701 | "bitflags 2.9.1", 702 | "crossterm 0.25.0", 703 | "dyn-clone", 704 | "fuzzy-matcher", 705 | "fxhash", 706 | "newline-converter", 707 | "once_cell", 708 | "unicode-segmentation", 709 | "unicode-width", 710 | ] 711 | 712 | [[package]] 713 | name = "is_terminal_polyfill" 714 | version = "1.70.1" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 717 | 718 | [[package]] 719 | name = "itertools" 720 | version = "0.13.0" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 723 | dependencies = [ 724 | "either", 725 | ] 726 | 727 | [[package]] 728 | name = "itoa" 729 | version = "1.0.15" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 732 | 733 | [[package]] 734 | name = "js-sys" 735 | version = "0.3.77" 736 | source = "registry+https://github.com/rust-lang/crates.io-index" 737 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 738 | dependencies = [ 739 | "once_cell", 740 | "wasm-bindgen", 741 | ] 742 | 743 | [[package]] 744 | name = "lazy-regex" 745 | version = "3.4.1" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "60c7310b93682b36b98fa7ea4de998d3463ccbebd94d935d6b48ba5b6ffa7126" 748 | dependencies = [ 749 | "lazy-regex-proc_macros", 750 | "once_cell", 751 | "regex", 752 | ] 753 | 754 | [[package]] 755 | name = "lazy-regex-proc_macros" 756 | version = "3.4.1" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "4ba01db5ef81e17eb10a5e0f2109d1b3a3e29bac3070fdbd7d156bf7dbd206a1" 759 | dependencies = [ 760 | "proc-macro2", 761 | "quote", 762 | "regex", 763 | "syn 2.0.104", 764 | ] 765 | 766 | [[package]] 767 | name = "libc" 768 | version = "0.2.174" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" 771 | 772 | [[package]] 773 | name = "libloading" 774 | version = "0.8.8" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 777 | dependencies = [ 778 | "cfg-if", 779 | "windows-targets 0.53.2", 780 | ] 781 | 782 | [[package]] 783 | name = "libproc" 784 | version = "0.14.10" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "e78a09b56be5adbcad5aa1197371688dc6bb249a26da3bca2011ee2fb987ebfb" 787 | dependencies = [ 788 | "bindgen 0.70.1", 789 | "errno", 790 | "libc", 791 | ] 792 | 793 | [[package]] 794 | name = "linux-raw-sys" 795 | version = "0.4.15" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 798 | 799 | [[package]] 800 | name = "linux-raw-sys" 801 | version = "0.9.4" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 804 | 805 | [[package]] 806 | name = "litrs" 807 | version = "0.4.1" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" 810 | 811 | [[package]] 812 | name = "lock_api" 813 | version = "0.4.13" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 816 | dependencies = [ 817 | "autocfg", 818 | "scopeguard", 819 | ] 820 | 821 | [[package]] 822 | name = "log" 823 | version = "0.4.27" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 826 | 827 | [[package]] 828 | name = "memchr" 829 | version = "2.7.5" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 832 | 833 | [[package]] 834 | name = "minimad" 835 | version = "0.13.1" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "a9c5d708226d186590a7b6d4a9780e2bdda5f689e0d58cd17012a298efd745d2" 838 | dependencies = [ 839 | "once_cell", 840 | ] 841 | 842 | [[package]] 843 | name = "minimal-lexical" 844 | version = "0.2.1" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 847 | 848 | [[package]] 849 | name = "miniz_oxide" 850 | version = "0.8.9" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 853 | dependencies = [ 854 | "adler2", 855 | ] 856 | 857 | [[package]] 858 | name = "mio" 859 | version = "0.8.11" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 862 | dependencies = [ 863 | "libc", 864 | "log", 865 | "wasi", 866 | "windows-sys 0.48.0", 867 | ] 868 | 869 | [[package]] 870 | name = "mio" 871 | version = "1.0.4" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" 874 | dependencies = [ 875 | "libc", 876 | "log", 877 | "wasi", 878 | "windows-sys 0.59.0", 879 | ] 880 | 881 | [[package]] 882 | name = "netlink-packet-core" 883 | version = "0.7.0" 884 | source = "registry+https://github.com/rust-lang/crates.io-index" 885 | checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4" 886 | dependencies = [ 887 | "anyhow", 888 | "byteorder", 889 | "netlink-packet-utils", 890 | ] 891 | 892 | [[package]] 893 | name = "netlink-packet-sock-diag" 894 | version = "0.4.2" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "a495cb1de50560a7cd12fdcf023db70eec00e340df81be31cedbbfd4aadd6b76" 897 | dependencies = [ 898 | "anyhow", 899 | "bitflags 1.3.2", 900 | "byteorder", 901 | "libc", 902 | "netlink-packet-core", 903 | "netlink-packet-utils", 904 | "smallvec", 905 | ] 906 | 907 | [[package]] 908 | name = "netlink-packet-utils" 909 | version = "0.5.2" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" 912 | dependencies = [ 913 | "anyhow", 914 | "byteorder", 915 | "paste", 916 | "thiserror 1.0.69", 917 | ] 918 | 919 | [[package]] 920 | name = "netlink-sys" 921 | version = "0.8.7" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23" 924 | dependencies = [ 925 | "bytes", 926 | "libc", 927 | "log", 928 | ] 929 | 930 | [[package]] 931 | name = "netstat2" 932 | version = "0.11.1" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "6422b6a8c7635e8a82323e4cdf07a90e91901e07f4c1f0f3a245d54b4637e55c" 935 | dependencies = [ 936 | "bindgen 0.71.1", 937 | "bitflags 2.9.1", 938 | "byteorder", 939 | "netlink-packet-core", 940 | "netlink-packet-sock-diag", 941 | "netlink-packet-utils", 942 | "netlink-sys", 943 | "num-derive", 944 | "num-traits", 945 | "thiserror 2.0.12", 946 | ] 947 | 948 | [[package]] 949 | name = "newline-converter" 950 | version = "0.3.0" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" 953 | dependencies = [ 954 | "unicode-segmentation", 955 | ] 956 | 957 | [[package]] 958 | name = "nix" 959 | version = "0.30.1" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 962 | dependencies = [ 963 | "bitflags 2.9.1", 964 | "cfg-if", 965 | "cfg_aliases", 966 | "libc", 967 | ] 968 | 969 | [[package]] 970 | name = "nom" 971 | version = "7.1.3" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 974 | dependencies = [ 975 | "memchr", 976 | "minimal-lexical", 977 | ] 978 | 979 | [[package]] 980 | name = "num-derive" 981 | version = "0.3.3" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" 984 | dependencies = [ 985 | "proc-macro2", 986 | "quote", 987 | "syn 1.0.109", 988 | ] 989 | 990 | [[package]] 991 | name = "num-modular" 992 | version = "0.6.1" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" 995 | 996 | [[package]] 997 | name = "num-order" 998 | version = "1.2.0" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" 1001 | dependencies = [ 1002 | "num-modular", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "num-traits" 1007 | version = "0.2.19" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1010 | dependencies = [ 1011 | "autocfg", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "once_cell" 1016 | version = "1.21.3" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1019 | 1020 | [[package]] 1021 | name = "once_cell_polyfill" 1022 | version = "1.70.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1025 | 1026 | [[package]] 1027 | name = "parking_lot" 1028 | version = "0.12.4" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" 1031 | dependencies = [ 1032 | "lock_api", 1033 | "parking_lot_core", 1034 | ] 1035 | 1036 | [[package]] 1037 | name = "parking_lot_core" 1038 | version = "0.9.11" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 1041 | dependencies = [ 1042 | "cfg-if", 1043 | "libc", 1044 | "redox_syscall", 1045 | "smallvec", 1046 | "windows-targets 0.52.6", 1047 | ] 1048 | 1049 | [[package]] 1050 | name = "paste" 1051 | version = "1.0.15" 1052 | source = "registry+https://github.com/rust-lang/crates.io-index" 1053 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1054 | 1055 | [[package]] 1056 | name = "pest" 1057 | version = "2.8.1" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" 1060 | dependencies = [ 1061 | "memchr", 1062 | "thiserror 2.0.12", 1063 | "ucd-trie", 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "pest_derive" 1068 | version = "2.8.1" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" 1071 | dependencies = [ 1072 | "pest", 1073 | "pest_generator", 1074 | ] 1075 | 1076 | [[package]] 1077 | name = "pest_generator" 1078 | version = "2.8.1" 1079 | source = "registry+https://github.com/rust-lang/crates.io-index" 1080 | checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" 1081 | dependencies = [ 1082 | "pest", 1083 | "pest_meta", 1084 | "proc-macro2", 1085 | "quote", 1086 | "syn 2.0.104", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "pest_meta" 1091 | version = "2.8.1" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" 1094 | dependencies = [ 1095 | "pest", 1096 | "sha2", 1097 | ] 1098 | 1099 | [[package]] 1100 | name = "prettyplease" 1101 | version = "0.2.35" 1102 | source = "registry+https://github.com/rust-lang/crates.io-index" 1103 | checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a" 1104 | dependencies = [ 1105 | "proc-macro2", 1106 | "syn 2.0.104", 1107 | ] 1108 | 1109 | [[package]] 1110 | name = "proc-macro2" 1111 | version = "1.0.95" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1114 | dependencies = [ 1115 | "unicode-ident", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "procfs" 1120 | version = "0.17.0" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" 1123 | dependencies = [ 1124 | "bitflags 2.9.1", 1125 | "chrono", 1126 | "flate2", 1127 | "hex", 1128 | "procfs-core", 1129 | "rustix 0.38.44", 1130 | ] 1131 | 1132 | [[package]] 1133 | name = "procfs-core" 1134 | version = "0.17.0" 1135 | source = "registry+https://github.com/rust-lang/crates.io-index" 1136 | checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" 1137 | dependencies = [ 1138 | "bitflags 2.9.1", 1139 | "chrono", 1140 | "hex", 1141 | ] 1142 | 1143 | [[package]] 1144 | name = "quote" 1145 | version = "1.0.40" 1146 | source = "registry+https://github.com/rust-lang/crates.io-index" 1147 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1148 | dependencies = [ 1149 | "proc-macro2", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "redox_syscall" 1154 | version = "0.5.13" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" 1157 | dependencies = [ 1158 | "bitflags 2.9.1", 1159 | ] 1160 | 1161 | [[package]] 1162 | name = "regex" 1163 | version = "1.11.1" 1164 | source = "registry+https://github.com/rust-lang/crates.io-index" 1165 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1166 | dependencies = [ 1167 | "aho-corasick", 1168 | "memchr", 1169 | "regex-automata", 1170 | "regex-syntax", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "regex-automata" 1175 | version = "0.4.9" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1178 | dependencies = [ 1179 | "aho-corasick", 1180 | "memchr", 1181 | "regex-syntax", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "regex-syntax" 1186 | version = "0.8.5" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1189 | 1190 | [[package]] 1191 | name = "rustc-hash" 1192 | version = "1.1.0" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1195 | 1196 | [[package]] 1197 | name = "rustc-hash" 1198 | version = "2.1.1" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1201 | 1202 | [[package]] 1203 | name = "rustix" 1204 | version = "0.38.44" 1205 | source = "registry+https://github.com/rust-lang/crates.io-index" 1206 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1207 | dependencies = [ 1208 | "bitflags 2.9.1", 1209 | "errno", 1210 | "libc", 1211 | "linux-raw-sys 0.4.15", 1212 | "windows-sys 0.59.0", 1213 | ] 1214 | 1215 | [[package]] 1216 | name = "rustix" 1217 | version = "1.0.8" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" 1220 | dependencies = [ 1221 | "bitflags 2.9.1", 1222 | "errno", 1223 | "libc", 1224 | "linux-raw-sys 0.9.4", 1225 | "windows-sys 0.60.2", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "rustversion" 1230 | version = "1.0.21" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 1233 | 1234 | [[package]] 1235 | name = "ryu" 1236 | version = "1.0.20" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1239 | 1240 | [[package]] 1241 | name = "scopeguard" 1242 | version = "1.2.0" 1243 | source = "registry+https://github.com/rust-lang/crates.io-index" 1244 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1245 | 1246 | [[package]] 1247 | name = "serde" 1248 | version = "1.0.219" 1249 | source = "registry+https://github.com/rust-lang/crates.io-index" 1250 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1251 | dependencies = [ 1252 | "serde_derive", 1253 | ] 1254 | 1255 | [[package]] 1256 | name = "serde_derive" 1257 | version = "1.0.219" 1258 | source = "registry+https://github.com/rust-lang/crates.io-index" 1259 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1260 | dependencies = [ 1261 | "proc-macro2", 1262 | "quote", 1263 | "syn 2.0.104", 1264 | ] 1265 | 1266 | [[package]] 1267 | name = "serde_json" 1268 | version = "1.0.140" 1269 | source = "registry+https://github.com/rust-lang/crates.io-index" 1270 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1271 | dependencies = [ 1272 | "itoa", 1273 | "memchr", 1274 | "ryu", 1275 | "serde", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "sha2" 1280 | version = "0.10.9" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" 1283 | dependencies = [ 1284 | "cfg-if", 1285 | "cpufeatures", 1286 | "digest", 1287 | ] 1288 | 1289 | [[package]] 1290 | name = "shlex" 1291 | version = "1.3.0" 1292 | source = "registry+https://github.com/rust-lang/crates.io-index" 1293 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1294 | 1295 | [[package]] 1296 | name = "signal-hook" 1297 | version = "0.3.18" 1298 | source = "registry+https://github.com/rust-lang/crates.io-index" 1299 | checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" 1300 | dependencies = [ 1301 | "libc", 1302 | "signal-hook-registry", 1303 | ] 1304 | 1305 | [[package]] 1306 | name = "signal-hook-mio" 1307 | version = "0.2.4" 1308 | source = "registry+https://github.com/rust-lang/crates.io-index" 1309 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 1310 | dependencies = [ 1311 | "libc", 1312 | "mio 0.8.11", 1313 | "mio 1.0.4", 1314 | "signal-hook", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "signal-hook-registry" 1319 | version = "1.4.5" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 1322 | dependencies = [ 1323 | "libc", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "smallvec" 1328 | version = "1.15.1" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1331 | 1332 | [[package]] 1333 | name = "somo" 1334 | version = "1.1.0" 1335 | dependencies = [ 1336 | "clap", 1337 | "clap_complete", 1338 | "handlebars", 1339 | "inquire", 1340 | "libproc", 1341 | "netstat2", 1342 | "nix", 1343 | "procfs", 1344 | "serde", 1345 | "serde_json", 1346 | "termimad", 1347 | ] 1348 | 1349 | [[package]] 1350 | name = "strict" 1351 | version = "0.2.0" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" 1354 | 1355 | [[package]] 1356 | name = "strsim" 1357 | version = "0.11.1" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1360 | 1361 | [[package]] 1362 | name = "syn" 1363 | version = "1.0.109" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1366 | dependencies = [ 1367 | "proc-macro2", 1368 | "quote", 1369 | "unicode-ident", 1370 | ] 1371 | 1372 | [[package]] 1373 | name = "syn" 1374 | version = "2.0.104" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" 1377 | dependencies = [ 1378 | "proc-macro2", 1379 | "quote", 1380 | "unicode-ident", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "termimad" 1385 | version = "0.33.0" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "b23646cdde4c2e9d6f2621424751b48653e5755f50163b2ff5ce3b32ebf17104" 1388 | dependencies = [ 1389 | "coolor", 1390 | "crokey", 1391 | "crossbeam", 1392 | "lazy-regex", 1393 | "minimad", 1394 | "serde", 1395 | "thiserror 2.0.12", 1396 | "unicode-width", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "thiserror" 1401 | version = "1.0.69" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 1404 | dependencies = [ 1405 | "thiserror-impl 1.0.69", 1406 | ] 1407 | 1408 | [[package]] 1409 | name = "thiserror" 1410 | version = "2.0.12" 1411 | source = "registry+https://github.com/rust-lang/crates.io-index" 1412 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1413 | dependencies = [ 1414 | "thiserror-impl 2.0.12", 1415 | ] 1416 | 1417 | [[package]] 1418 | name = "thiserror-impl" 1419 | version = "1.0.69" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 1422 | dependencies = [ 1423 | "proc-macro2", 1424 | "quote", 1425 | "syn 2.0.104", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "thiserror-impl" 1430 | version = "2.0.12" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 1433 | dependencies = [ 1434 | "proc-macro2", 1435 | "quote", 1436 | "syn 2.0.104", 1437 | ] 1438 | 1439 | [[package]] 1440 | name = "thread_local" 1441 | version = "1.1.9" 1442 | source = "registry+https://github.com/rust-lang/crates.io-index" 1443 | checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 1444 | dependencies = [ 1445 | "cfg-if", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "typenum" 1450 | version = "1.18.0" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" 1453 | 1454 | [[package]] 1455 | name = "ucd-trie" 1456 | version = "0.1.7" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" 1459 | 1460 | [[package]] 1461 | name = "unicode-ident" 1462 | version = "1.0.18" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 1465 | 1466 | [[package]] 1467 | name = "unicode-segmentation" 1468 | version = "1.12.0" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 1471 | 1472 | [[package]] 1473 | name = "unicode-width" 1474 | version = "0.1.14" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 1477 | 1478 | [[package]] 1479 | name = "utf8parse" 1480 | version = "0.2.2" 1481 | source = "registry+https://github.com/rust-lang/crates.io-index" 1482 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 1483 | 1484 | [[package]] 1485 | name = "version_check" 1486 | version = "0.9.5" 1487 | source = "registry+https://github.com/rust-lang/crates.io-index" 1488 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 1489 | 1490 | [[package]] 1491 | name = "wasi" 1492 | version = "0.11.1+wasi-snapshot-preview1" 1493 | source = "registry+https://github.com/rust-lang/crates.io-index" 1494 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 1495 | 1496 | [[package]] 1497 | name = "wasm-bindgen" 1498 | version = "0.2.100" 1499 | source = "registry+https://github.com/rust-lang/crates.io-index" 1500 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 1501 | dependencies = [ 1502 | "cfg-if", 1503 | "once_cell", 1504 | "rustversion", 1505 | "wasm-bindgen-macro", 1506 | ] 1507 | 1508 | [[package]] 1509 | name = "wasm-bindgen-backend" 1510 | version = "0.2.100" 1511 | source = "registry+https://github.com/rust-lang/crates.io-index" 1512 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 1513 | dependencies = [ 1514 | "bumpalo", 1515 | "log", 1516 | "proc-macro2", 1517 | "quote", 1518 | "syn 2.0.104", 1519 | "wasm-bindgen-shared", 1520 | ] 1521 | 1522 | [[package]] 1523 | name = "wasm-bindgen-macro" 1524 | version = "0.2.100" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 1527 | dependencies = [ 1528 | "quote", 1529 | "wasm-bindgen-macro-support", 1530 | ] 1531 | 1532 | [[package]] 1533 | name = "wasm-bindgen-macro-support" 1534 | version = "0.2.100" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 1537 | dependencies = [ 1538 | "proc-macro2", 1539 | "quote", 1540 | "syn 2.0.104", 1541 | "wasm-bindgen-backend", 1542 | "wasm-bindgen-shared", 1543 | ] 1544 | 1545 | [[package]] 1546 | name = "wasm-bindgen-shared" 1547 | version = "0.2.100" 1548 | source = "registry+https://github.com/rust-lang/crates.io-index" 1549 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 1550 | dependencies = [ 1551 | "unicode-ident", 1552 | ] 1553 | 1554 | [[package]] 1555 | name = "winapi" 1556 | version = "0.3.9" 1557 | source = "registry+https://github.com/rust-lang/crates.io-index" 1558 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1559 | dependencies = [ 1560 | "winapi-i686-pc-windows-gnu", 1561 | "winapi-x86_64-pc-windows-gnu", 1562 | ] 1563 | 1564 | [[package]] 1565 | name = "winapi-i686-pc-windows-gnu" 1566 | version = "0.4.0" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1569 | 1570 | [[package]] 1571 | name = "winapi-x86_64-pc-windows-gnu" 1572 | version = "0.4.0" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1575 | 1576 | [[package]] 1577 | name = "windows-core" 1578 | version = "0.61.2" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 1581 | dependencies = [ 1582 | "windows-implement", 1583 | "windows-interface", 1584 | "windows-link", 1585 | "windows-result", 1586 | "windows-strings", 1587 | ] 1588 | 1589 | [[package]] 1590 | name = "windows-implement" 1591 | version = "0.60.0" 1592 | source = "registry+https://github.com/rust-lang/crates.io-index" 1593 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 1594 | dependencies = [ 1595 | "proc-macro2", 1596 | "quote", 1597 | "syn 2.0.104", 1598 | ] 1599 | 1600 | [[package]] 1601 | name = "windows-interface" 1602 | version = "0.59.1" 1603 | source = "registry+https://github.com/rust-lang/crates.io-index" 1604 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 1605 | dependencies = [ 1606 | "proc-macro2", 1607 | "quote", 1608 | "syn 2.0.104", 1609 | ] 1610 | 1611 | [[package]] 1612 | name = "windows-link" 1613 | version = "0.1.3" 1614 | source = "registry+https://github.com/rust-lang/crates.io-index" 1615 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 1616 | 1617 | [[package]] 1618 | name = "windows-result" 1619 | version = "0.3.4" 1620 | source = "registry+https://github.com/rust-lang/crates.io-index" 1621 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 1622 | dependencies = [ 1623 | "windows-link", 1624 | ] 1625 | 1626 | [[package]] 1627 | name = "windows-strings" 1628 | version = "0.4.2" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 1631 | dependencies = [ 1632 | "windows-link", 1633 | ] 1634 | 1635 | [[package]] 1636 | name = "windows-sys" 1637 | version = "0.48.0" 1638 | source = "registry+https://github.com/rust-lang/crates.io-index" 1639 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 1640 | dependencies = [ 1641 | "windows-targets 0.48.5", 1642 | ] 1643 | 1644 | [[package]] 1645 | name = "windows-sys" 1646 | version = "0.59.0" 1647 | source = "registry+https://github.com/rust-lang/crates.io-index" 1648 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1649 | dependencies = [ 1650 | "windows-targets 0.52.6", 1651 | ] 1652 | 1653 | [[package]] 1654 | name = "windows-sys" 1655 | version = "0.60.2" 1656 | source = "registry+https://github.com/rust-lang/crates.io-index" 1657 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1658 | dependencies = [ 1659 | "windows-targets 0.53.2", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "windows-targets" 1664 | version = "0.48.5" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1667 | dependencies = [ 1668 | "windows_aarch64_gnullvm 0.48.5", 1669 | "windows_aarch64_msvc 0.48.5", 1670 | "windows_i686_gnu 0.48.5", 1671 | "windows_i686_msvc 0.48.5", 1672 | "windows_x86_64_gnu 0.48.5", 1673 | "windows_x86_64_gnullvm 0.48.5", 1674 | "windows_x86_64_msvc 0.48.5", 1675 | ] 1676 | 1677 | [[package]] 1678 | name = "windows-targets" 1679 | version = "0.52.6" 1680 | source = "registry+https://github.com/rust-lang/crates.io-index" 1681 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1682 | dependencies = [ 1683 | "windows_aarch64_gnullvm 0.52.6", 1684 | "windows_aarch64_msvc 0.52.6", 1685 | "windows_i686_gnu 0.52.6", 1686 | "windows_i686_gnullvm 0.52.6", 1687 | "windows_i686_msvc 0.52.6", 1688 | "windows_x86_64_gnu 0.52.6", 1689 | "windows_x86_64_gnullvm 0.52.6", 1690 | "windows_x86_64_msvc 0.52.6", 1691 | ] 1692 | 1693 | [[package]] 1694 | name = "windows-targets" 1695 | version = "0.53.2" 1696 | source = "registry+https://github.com/rust-lang/crates.io-index" 1697 | checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" 1698 | dependencies = [ 1699 | "windows_aarch64_gnullvm 0.53.0", 1700 | "windows_aarch64_msvc 0.53.0", 1701 | "windows_i686_gnu 0.53.0", 1702 | "windows_i686_gnullvm 0.53.0", 1703 | "windows_i686_msvc 0.53.0", 1704 | "windows_x86_64_gnu 0.53.0", 1705 | "windows_x86_64_gnullvm 0.53.0", 1706 | "windows_x86_64_msvc 0.53.0", 1707 | ] 1708 | 1709 | [[package]] 1710 | name = "windows_aarch64_gnullvm" 1711 | version = "0.48.5" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1714 | 1715 | [[package]] 1716 | name = "windows_aarch64_gnullvm" 1717 | version = "0.52.6" 1718 | source = "registry+https://github.com/rust-lang/crates.io-index" 1719 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1720 | 1721 | [[package]] 1722 | name = "windows_aarch64_gnullvm" 1723 | version = "0.53.0" 1724 | source = "registry+https://github.com/rust-lang/crates.io-index" 1725 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 1726 | 1727 | [[package]] 1728 | name = "windows_aarch64_msvc" 1729 | version = "0.48.5" 1730 | source = "registry+https://github.com/rust-lang/crates.io-index" 1731 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1732 | 1733 | [[package]] 1734 | name = "windows_aarch64_msvc" 1735 | version = "0.52.6" 1736 | source = "registry+https://github.com/rust-lang/crates.io-index" 1737 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1738 | 1739 | [[package]] 1740 | name = "windows_aarch64_msvc" 1741 | version = "0.53.0" 1742 | source = "registry+https://github.com/rust-lang/crates.io-index" 1743 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 1744 | 1745 | [[package]] 1746 | name = "windows_i686_gnu" 1747 | version = "0.48.5" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1750 | 1751 | [[package]] 1752 | name = "windows_i686_gnu" 1753 | version = "0.52.6" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1756 | 1757 | [[package]] 1758 | name = "windows_i686_gnu" 1759 | version = "0.53.0" 1760 | source = "registry+https://github.com/rust-lang/crates.io-index" 1761 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 1762 | 1763 | [[package]] 1764 | name = "windows_i686_gnullvm" 1765 | version = "0.52.6" 1766 | source = "registry+https://github.com/rust-lang/crates.io-index" 1767 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1768 | 1769 | [[package]] 1770 | name = "windows_i686_gnullvm" 1771 | version = "0.53.0" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 1774 | 1775 | [[package]] 1776 | name = "windows_i686_msvc" 1777 | version = "0.48.5" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1780 | 1781 | [[package]] 1782 | name = "windows_i686_msvc" 1783 | version = "0.52.6" 1784 | source = "registry+https://github.com/rust-lang/crates.io-index" 1785 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1786 | 1787 | [[package]] 1788 | name = "windows_i686_msvc" 1789 | version = "0.53.0" 1790 | source = "registry+https://github.com/rust-lang/crates.io-index" 1791 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 1792 | 1793 | [[package]] 1794 | name = "windows_x86_64_gnu" 1795 | version = "0.48.5" 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" 1797 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1798 | 1799 | [[package]] 1800 | name = "windows_x86_64_gnu" 1801 | version = "0.52.6" 1802 | source = "registry+https://github.com/rust-lang/crates.io-index" 1803 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1804 | 1805 | [[package]] 1806 | name = "windows_x86_64_gnu" 1807 | version = "0.53.0" 1808 | source = "registry+https://github.com/rust-lang/crates.io-index" 1809 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 1810 | 1811 | [[package]] 1812 | name = "windows_x86_64_gnullvm" 1813 | version = "0.48.5" 1814 | source = "registry+https://github.com/rust-lang/crates.io-index" 1815 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1816 | 1817 | [[package]] 1818 | name = "windows_x86_64_gnullvm" 1819 | version = "0.52.6" 1820 | source = "registry+https://github.com/rust-lang/crates.io-index" 1821 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1822 | 1823 | [[package]] 1824 | name = "windows_x86_64_gnullvm" 1825 | version = "0.53.0" 1826 | source = "registry+https://github.com/rust-lang/crates.io-index" 1827 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 1828 | 1829 | [[package]] 1830 | name = "windows_x86_64_msvc" 1831 | version = "0.48.5" 1832 | source = "registry+https://github.com/rust-lang/crates.io-index" 1833 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1834 | 1835 | [[package]] 1836 | name = "windows_x86_64_msvc" 1837 | version = "0.52.6" 1838 | source = "registry+https://github.com/rust-lang/crates.io-index" 1839 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1840 | 1841 | [[package]] 1842 | name = "windows_x86_64_msvc" 1843 | version = "0.53.0" 1844 | source = "registry+https://github.com/rust-lang/crates.io-index" 1845 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 1846 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "somo" 3 | version = "1.1.0" 4 | edition = "2021" 5 | authors = ["theopfr"] 6 | description = "A human-friendly alternative to netstat for socket and port monitoring on Linux and macOS." 7 | license = "MIT" 8 | readme = "./README.md" 9 | repository = "https://github.com/theopfr/somo/" 10 | keywords = ["netstat", "socket-monitoring", "port-checker"] 11 | categories = ["command-line-utilities"] 12 | 13 | [dependencies] 14 | clap = { version = "4.5.40", features = ["derive"] } 15 | clap_complete = "4.5.54" 16 | inquire = "0.7.5" 17 | termimad = "0.33.0" 18 | serde = { version = "1.0.196", features = ["derive"] } 19 | serde_json = "1.0.96" 20 | handlebars = "6.3.2" 21 | nix = {version = "0.30.1", features = ["process", "signal"]} 22 | 23 | [target.'cfg(target_os = "linux")'.dependencies] 24 | procfs = "0.17.0" 25 | 26 | [target.'cfg(target_os = "macos")'.dependencies] 27 | netstat2 = "0.11.1" 28 | libproc = "0.14.10" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Theodor Peifer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <p align="center"> 2 | <img style="width: 250px" src="./images/somo-logo.png" /> 3 | </p> 4 | 5 | 6 | ### A human-friendly alternative to netstat for socket and port monitoring on Linux and macOS. 7 | 8 | > [!NOTE] 9 | > The master branch code and readme may include features that are not yet released. For the official, stable version and its documentation, please refer to the [crates.io](https://crates.io/crates/somo) page. 10 | 11 | ## ✨ Highlights: 12 | - pleasing to the eye thanks to a nice table view 13 | - filterable and sortable output 14 | - interactive killing of processes 15 | - JSON and custom formattable output 16 | - from ``netstat -tulpn`` to ``somo -l`` 17 | - cross-platform support for Linux and macOS 18 | - you can find all features further down 19 | 20 | <br /> 21 | 22 | <p align="center"> 23 | <img src="./images/somo-example.png" /> 24 | </p> 25 | 26 | ## ⬇️ Installation: 27 | 28 | ### Option 1 - Debian: 29 | If you use a Debian OS go to [releases](https://github.com/theopfr/somo/releases) and download the latest .deb release. 30 | 31 | ### Option 2 - crates.io: 32 | ```sh 33 | cargo install somo 34 | ``` 35 | Most of the time you will want to run this in ``sudo`` mode to see all processes and ports. By default, this is not possible when installed via cargo. But you can create a symlink so the binary can be run as root: 36 | ```sh 37 | sudo ln -s ~/.cargo/bin/somo /usr/local/bin/somo 38 | sudo somo # this works now 39 | ``` 40 | 41 | ### Option 3 - GitHub (Development Version): 42 | *Warning:* This is the cutting-edge development version and may be unstable or contain incomplete features. You can install it via cargo directly from the GitHub repository. 43 | 44 | ```sh 45 | cargo install --git https://github.com/theopfr/somo 46 | ``` 47 | 48 | ### Option 4 - Nix (Development Version): 49 | You can build and use the development version using Nix with Flakes. 50 | ```sh 51 | nix build 'github:theopfr/somo?dir=nix' 52 | sudo ./result/bin/somo 53 | ``` 54 | 55 | --- 56 | 57 | ## 🏃♀️ Running somo: 58 | To run somo just type: 59 | ```sh 60 | sudo somo 61 | ``` 62 | 63 | Somo supports the following features: 64 | 65 | ### ✨ Filtering: 66 | You can use the following flags to filter based on different attributes: 67 | | filter flag | description | value | 68 | | :------------- |:------------- | :----- | 69 | | ```--tcp, -t``` | filter by TCP connections | - | 70 | | ```--udp, -u``` | filter by UDP connections | - | 71 | | ```--proto``` | deprecated – use ``--tcp`` / ``--udp`` instead | ``tcp`` or ``udp`` | 72 | | ```--port, -p``` | filter by a local port | port number, e.g ``5433`` | 73 | | ```--remote-port``` | filter by a remote port | port number, e.g ``443`` | 74 | | ```--ip``` | filter by a remote IP | IP address e.g ``0.0.0.0`` | 75 | | ```--program``` | filter by a client program | program name e.g ``chrome`` | 76 | | ```--pid``` | filter by a PID | PID number, e.g ``10000`` | 77 | | ```--open, -o``` | filter by open connections | - | 78 | | ```--listen, -l``` | filter by listening connections | - | 79 | | ```--exclude-ipv6``` | don't list IPv6 connections | - | 80 | 81 | ### ✨ Compact table view: 82 | To get a smaller, more compact table use the ``--compact, -c`` flag. 83 | 84 | <img style="width: 75%" src="./images/somo-compact-example.png" /> 85 | 86 | ### ✨ Process killing: 87 | With the ``--kill, -k`` flag you can choose to kill a process after inspecting the connections using an interactive selection option. 88 | 89 | <img style="width: 75%" src="./images/somo-kill-example.png" /> 90 | 91 | ### ✨ JSON and custom output format: 92 | Using the ``--json`` flag you can choose to retrieve the connection data in JSON format. <br /> 93 | You can also define a custom output format using the ``--format`` flag, for example: 94 | ```sh 95 | somo --format "PID: {{pid}}, Protocol: {{proto}}, Remote Address: {{remote_address}}" # attributes must be specified in snake_case 96 | ``` 97 | In the format-string, the attributes have to be specified in *snake_case*. 98 | 99 | ### ✨ Sorting by columns: 100 | The ``--sort, -s`` flag can be used to sort the table after a specific column ascending. For example: 101 | ```sh 102 | somo --sort pid # column names must be specified in snake_case 103 | ``` 104 | To get a descending order, you can use the ``--reverse, -r`` flag. 105 | 106 | --- 107 | 108 | ## 🐚 Shell Completions: 109 | Somo supports shell completions for bash, zsh, fish, and elvish. Choose your shell: 110 | 111 | #### Bash 112 | ```bash 113 | mkdir -p ~/.local/share/bash-completion/completions 114 | somo generate-completions bash > ~/.local/share/bash-completion/completions/somo 115 | ``` 116 | 117 | #### Zsh 118 | ```zsh 119 | mkdir -p ~/.local/share/zsh/site-functions 120 | somo generate-completions zsh > ~/.local/share/zsh/site-functions/_somo 121 | echo 'fpath=(~/.local/share/zsh/site-functions $fpath)' >> ~/.zshrc 122 | echo 'autoload -U compinit && compinit' >> ~/.zshrc 123 | ``` 124 | 125 | #### Fish 126 | ```fish 127 | mkdir -p ~/.config/fish/completions 128 | somo generate-completions fish > ~/.config/fish/completions/somo.fish 129 | ``` 130 | 131 | #### Elvish 132 | ```bash 133 | mkdir -p ~/.config/elvish/lib 134 | somo generate-completions elvish > ~/.config/elvish/lib/somo.elv 135 | echo 'use somo' >> ~/.config/elvish/rc.elv 136 | ``` 137 | 138 | --- 139 | 140 | ## 🖥️ Platform Support: 141 | Somo currently supports: 142 | - Linux: Full support using the [procfs](https://crates.io/crates/procfs) crate 143 | - macOS: Full support using [netstat2](https://crates.io/crates/netstat2) and [libproc](https://crates.io/crates/libproc/0.13.0) crates 144 | -------------------------------------------------------------------------------- /images/somo-compact-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-compact-example.png -------------------------------------------------------------------------------- /images/somo-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-example.png -------------------------------------------------------------------------------- /images/somo-kill-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-kill-example.png -------------------------------------------------------------------------------- /images/somo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theopfr/somo/752419df33470c35e4f3f78f2e66e7de87d409d2/images/somo-logo.png -------------------------------------------------------------------------------- /nix/flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1752596105, 6 | "narHash": "sha256-lFNVsu/mHLq3q11MuGkMhUUoSXEdQjCHvpReaGP1S2k=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "dab3a6e781554f965bde3def0aa2fda4eb8f1708", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /nix/flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | }; 5 | 6 | outputs = 7 | { 8 | nixpkgs, 9 | ... 10 | }: 11 | let 12 | lib = nixpkgs.lib; 13 | forAllSystems = lib.genAttrs lib.systems.flakeExposed; 14 | in 15 | { 16 | formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixfmt-tree); 17 | 18 | packages = forAllSystems ( 19 | system: 20 | let 21 | pkgs = nixpkgs.legacyPackages.${system}; 22 | in 23 | { 24 | default = pkgs.callPackage ./package.nix { }; 25 | } 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /nix/package.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | rustPlatform, 4 | versionCheckHook, 5 | }: 6 | 7 | let 8 | mainProgram = "somo"; 9 | in 10 | rustPlatform.buildRustPackage (finalAttrs: { 11 | pname = "somo"; 12 | version = with builtins; (fromTOML (readFile ../Cargo.toml)).package.version; 13 | 14 | src = lib.fileset.toSource { 15 | root = ./..; 16 | fileset = lib.fileset.unions [ 17 | ../src 18 | ../Cargo.toml 19 | ../Cargo.lock 20 | ]; 21 | }; 22 | 23 | cargoLock.lockFile = ../Cargo.lock; 24 | 25 | nativeInstallCheckInputs = [ 26 | versionCheckHook 27 | ]; 28 | doInstallCheck = true; 29 | versionCheckProgram = "${placeholder "out"}/bin/${mainProgram}"; 30 | versionCheckProgramArg = "--version"; 31 | 32 | meta = { 33 | inherit mainProgram; 34 | description = "Human-friendly alternative to netstat for socket and port monitoring"; 35 | homepage = "https://github.com/theopfr/somo"; 36 | license = lib.licenses.mit; 37 | platforms = lib.platforms.linux ++ lib.platforms.darwin; 38 | }; 39 | }) 40 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use clap_complete::{generate, Generator, Shell}; 3 | use inquire::InquireError; 4 | use inquire::Select; 5 | use nix::sys::signal; 6 | use nix::unistd::Pid; 7 | use std::str::FromStr; 8 | use std::{io, string::String}; 9 | 10 | use crate::schemas::{Connection, Protocol, Protocols}; 11 | use crate::utils; 12 | 13 | /// Used for parsing all the flag values provided by the user in the CLI. 14 | #[derive(Debug, Default)] 15 | pub struct Flags { 16 | pub kill: bool, 17 | pub proto: Option<String>, 18 | pub tcp: bool, 19 | pub udp: bool, 20 | pub ip: Option<String>, 21 | pub remote_port: Option<String>, 22 | pub port: Option<String>, 23 | pub program: Option<String>, 24 | pub pid: Option<String>, 25 | pub format: Option<String>, 26 | pub json: bool, 27 | pub open: bool, 28 | pub listen: bool, 29 | pub exclude_ipv6: bool, 30 | pub compact: bool, 31 | pub sort: Option<SortField>, 32 | pub reverse: bool, 33 | } 34 | 35 | /// Represents all possible flags which can be provided by the user in the CLI. 36 | #[derive(Parser, Debug)] 37 | #[command(author, version, about, long_about = None)] 38 | pub struct Args { 39 | #[command(subcommand)] 40 | command: Option<Commands>, 41 | 42 | /// Display an interactive selection option after inspecting connections 43 | #[arg(short = 'k', long, default_value = None)] 44 | kill: bool, 45 | 46 | /// Deprecated. Use '--tcp' and '--udp' instead. 47 | #[arg(long, default_value = None)] 48 | proto: Option<String>, 49 | 50 | /// Include TCP connections 51 | #[arg(short, long, default_value = None)] 52 | tcp: bool, 53 | 54 | /// Include UDP connections 55 | #[arg(short, long, default_value = None)] 56 | udp: bool, 57 | 58 | /// Filter connections by remote IP address 59 | #[arg(long, default_value = None)] 60 | ip: Option<String>, 61 | 62 | /// Filter connections by remote port 63 | #[arg(long, default_value = None)] 64 | remote_port: Option<String>, 65 | 66 | /// Filter connections by local port 67 | #[arg(short = 'p', long, default_value = None)] 68 | port: Option<String>, 69 | 70 | /// Filter connections by program name 71 | #[arg(long, default_value = None)] 72 | program: Option<String>, 73 | 74 | /// Filter connections by PID 75 | #[arg(long, default_value = None)] 76 | pid: Option<String>, 77 | 78 | /// Format the output in a certain way, e.g., `somo --format "PID: {{pid}}, Protocol: {{proto}}, Remote Address: {{remote_address}}"` 79 | #[arg(long, default_value = None)] 80 | format: Option<String>, 81 | 82 | /// Output in JSON 83 | #[arg(long, default_value_t = false)] 84 | json: bool, 85 | 86 | /// Filter by open connections 87 | #[arg(short = 'o', long, default_value_t = false)] 88 | open: bool, 89 | 90 | /// Filter by listening connections 91 | #[arg(short = 'l', long, default_value_t = false)] 92 | listen: bool, 93 | 94 | /// Exclude IPv6 connections 95 | #[arg(long, default_value_t = false)] 96 | exclude_ipv6: bool, 97 | 98 | #[arg(short = 'c', long, default_value_t = false)] 99 | compact: bool, 100 | 101 | /// Reverse order of the table 102 | #[arg(short = 'r', long, default_value_t = false)] 103 | reverse: bool, 104 | 105 | /// Sort by column name 106 | #[arg(short = 's', long, default_value = None)] 107 | sort: Option<SortField>, 108 | } 109 | 110 | #[derive(Subcommand, Debug)] 111 | pub enum Commands { 112 | /// Generate shell completions 113 | GenerateCompletions { 114 | /// The shell to generate completions for 115 | #[arg(value_enum)] 116 | shell: Shell, 117 | }, 118 | } 119 | 120 | pub enum CliCommand { 121 | Run(Flags), 122 | Subcommand(Commands), 123 | } 124 | 125 | #[derive(clap::ValueEnum, Clone, Copy, Debug)] 126 | #[clap(rename_all = "snake_case")] 127 | pub enum SortField { 128 | Proto, 129 | LocalPort, 130 | RemoteAddress, 131 | RemotePort, 132 | Program, 133 | Pid, 134 | State, 135 | } 136 | 137 | /// Gets all flag values provided by the user in the CLI using the "clap" crate. 138 | /// 139 | /// # Arguments 140 | /// None 141 | /// 142 | /// # Returns 143 | /// A `CliCommand` enum which contains either the `Run` variant with the parsed flags or the `Subcommand` variant with a specific command. 144 | pub fn cli() -> CliCommand { 145 | let args = Args::parse(); 146 | 147 | match args.command { 148 | Some(cmd) => CliCommand::Subcommand(cmd), 149 | None => CliCommand::Run(Flags { 150 | kill: args.kill, 151 | proto: args.proto, 152 | tcp: args.tcp, 153 | udp: args.udp, 154 | ip: args.ip, 155 | remote_port: args.remote_port, 156 | port: args.port, 157 | program: args.program, 158 | pid: args.pid, 159 | format: args.format, 160 | json: args.json, 161 | open: args.open, 162 | listen: args.listen, 163 | exclude_ipv6: args.exclude_ipv6, 164 | compact: args.compact, 165 | sort: args.sort, 166 | reverse: args.reverse, 167 | }), 168 | } 169 | } 170 | 171 | pub fn sort_connections(all_connections: &mut [Connection], field: SortField) { 172 | all_connections.sort_by(|our, other| match field { 173 | SortField::Proto => our.proto.to_lowercase().cmp(&other.proto.to_lowercase()), 174 | SortField::LocalPort => our 175 | .local_port 176 | .parse::<u32>() 177 | .unwrap_or(0) 178 | .cmp(&other.local_port.parse::<u32>().unwrap_or(0)), 179 | SortField::RemoteAddress => our.ipvx_raw.cmp(&other.ipvx_raw), 180 | SortField::RemotePort => our 181 | .remote_port 182 | .parse::<u32>() 183 | .unwrap_or(0) 184 | .cmp(&other.remote_port.parse::<u32>().unwrap_or(0)), 185 | SortField::Program => our 186 | .program 187 | .to_lowercase() 188 | .cmp(&other.program.to_lowercase()), 189 | SortField::Pid => our.pid.cmp(&other.pid), 190 | SortField::State => our.state.to_lowercase().cmp(&other.state.to_lowercase()), 191 | }); 192 | } 193 | 194 | /// Determines which protocols to include based on CLI flags. 195 | /// 196 | /// The `--tcp` and `--udp` flags take precedence over the deprecated `--proto` flag. 197 | /// If either `--tcp` or `--udp` is set, `--proto` is ignored. 198 | /// If no relevant flags are set, both TCP and UDP are enabled by default. 199 | /// 200 | /// # Arguments 201 | /// * `args`: Parsed CLI flags (of interest: `--tcp`, `--udp`, and optionally `--proto`) 202 | /// 203 | /// # Returns 204 | /// A `Protocols` struct indicating whether to include TCP, UDP, or both. 205 | pub fn resolve_protocols(args: &Flags) -> Protocols { 206 | let mut protocols = Protocols::default(); 207 | if args.tcp || args.udp { 208 | protocols.tcp = args.tcp; 209 | protocols.udp = args.udp; 210 | } else if let Some(arg) = &args.proto { 211 | // Support the deprecated '--proto' argument 212 | if let Ok(matching) = Protocol::from_str(arg) { 213 | match matching { 214 | Protocol::Tcp => protocols.tcp = true, 215 | Protocol::Udp => protocols.udp = true, 216 | } 217 | } 218 | } else { 219 | protocols.tcp = true; 220 | protocols.udp = true; 221 | } 222 | protocols 223 | } 224 | 225 | /// Generates and prints shell completions to stdout. 226 | /// 227 | /// # Arguments 228 | /// * `gen` - The shell to generate completions for 229 | /// * `cmd` - The clap command to generate completions for 230 | /// 231 | /// # Returns 232 | /// None 233 | pub fn print_completions<G: Generator>(gen: G, cmd: &mut clap::Command) { 234 | generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout()); 235 | } 236 | 237 | /// Kills a process by its PID. 238 | /// 239 | /// # Argument 240 | /// * `pid`: The PID value as a string. 241 | /// 242 | /// # Returns 243 | /// None 244 | pub fn kill_process(pid_num: i32) { 245 | let pid = Pid::from_raw(pid_num); 246 | 247 | match signal::kill(pid, signal::Signal::SIGTERM) { 248 | Ok(_) => utils::pretty_print_info(&format!("Killed process with PID {pid}.")), 249 | Err(_) => utils::pretty_print_error(&format!("Failed to kill process with PID {pid}.")), 250 | } 251 | } 252 | 253 | /// Starts an interactive selection process in the console for choosing a process to kill using the "inquire" crate. 254 | /// 255 | /// # Argument 256 | /// * `connections`: A vector containing all connections which themselves contain a PID value. 257 | /// 258 | /// # Returns 259 | /// None 260 | pub fn interactive_process_kill(connections: &[Connection]) { 261 | let selection: Result<u32, InquireError> = Select::new( 262 | "Which process to kill (search or type index)?", 263 | (1..=connections.len() as u32).collect(), 264 | ) 265 | .prompt(); 266 | 267 | match selection { 268 | Ok(choice) => { 269 | let pid_str = &connections[choice as usize - 1].pid; 270 | let pid_num = match pid_str.parse::<i32>() { 271 | Ok(pid) => pid, 272 | Err(_) => { 273 | utils::pretty_print_error("Couldn't find PID."); 274 | return; 275 | } 276 | }; 277 | kill_process(pid_num) 278 | } 279 | Err(_) => { 280 | utils::pretty_print_error("Process selection cancelled."); 281 | } 282 | } 283 | } 284 | 285 | #[cfg(test)] 286 | mod tests { 287 | use std::{net::IpAddr, str::FromStr}; 288 | 289 | use crate::{ 290 | cli::{resolve_protocols, sort_connections, SortField}, 291 | schemas::AddressType, 292 | }; 293 | 294 | use super::{Args, Commands, Flags}; 295 | use clap::Parser; 296 | 297 | #[test] 298 | fn test_all_flags_parsing() { 299 | let args = Args::parse_from([ 300 | "test-bin", 301 | "-k", 302 | "--proto", 303 | "udp", 304 | "--tcp", 305 | "--udp", 306 | "--ip", 307 | "192.168.0.1", 308 | "--remote-port", 309 | "53", 310 | "-p", 311 | "8080", 312 | "--program", 313 | "nginx", 314 | "--pid", 315 | "1234", 316 | "-o", 317 | "-l", 318 | "--exclude-ipv6", 319 | ]); 320 | 321 | assert!(args.kill); 322 | assert_eq!(args.proto.as_deref(), Some("udp")); 323 | assert!(args.tcp); 324 | assert!(args.udp); 325 | assert_eq!(args.ip.as_deref(), Some("192.168.0.1")); 326 | assert_eq!(args.remote_port.as_deref(), Some("53")); 327 | assert_eq!(args.port.as_deref(), Some("8080")); 328 | assert_eq!(args.program.as_deref(), Some("nginx")); 329 | assert_eq!(args.pid.as_deref(), Some("1234")); 330 | assert!(args.open); 331 | assert!(args.listen); 332 | assert!(args.exclude_ipv6); 333 | } 334 | 335 | #[test] 336 | fn test_default_values() { 337 | let args = Args::parse_from(["test-bin"]); 338 | 339 | assert!(!args.kill); 340 | assert!(args.proto.is_none()); 341 | assert!(!args.tcp); 342 | assert!(!args.udp); 343 | assert!(args.ip.is_none()); 344 | assert!(args.remote_port.is_none()); 345 | assert!(args.port.is_none()); 346 | assert!(args.program.is_none()); 347 | assert!(args.pid.is_none()); 348 | assert!(!args.open); 349 | assert!(!args.listen); 350 | assert!(!args.exclude_ipv6); 351 | } 352 | 353 | #[test] 354 | fn test_flag_short_and_long_equivalence() { 355 | let short = Args::parse_from(["test-bin", "-k", "-p", "80", "-o", "-l"]); 356 | let long = Args::parse_from(["test-bin", "--kill", "--port", "80", "--open", "--listen"]); 357 | 358 | assert_eq!(short.kill, long.kill); 359 | assert_eq!(short.port, long.port); 360 | assert_eq!(short.open, long.open); 361 | assert_eq!(short.listen, long.listen); 362 | assert_eq!(short.exclude_ipv6, long.exclude_ipv6); 363 | } 364 | 365 | #[test] 366 | fn test_resolve_protocols() { 367 | // Test deprecated --proto 368 | let flags = Flags { 369 | tcp: false, 370 | udp: false, 371 | proto: Some("tcp".into()), 372 | ..Default::default() 373 | }; 374 | let result = resolve_protocols(&flags); 375 | assert!(result.tcp); 376 | assert!(!result.udp); 377 | 378 | // Test precendence of --tcp/--udp over --proto 379 | let flags = Flags { 380 | tcp: true, 381 | udp: false, 382 | proto: Some("udp".into()), 383 | ..Default::default() 384 | }; 385 | let result = resolve_protocols(&flags); 386 | assert!(result.tcp); 387 | assert!(!result.udp); 388 | 389 | // Test default with no protocol flags 390 | let flags = Flags { 391 | tcp: false, 392 | udp: false, 393 | proto: None, 394 | ..Default::default() 395 | }; 396 | let result = resolve_protocols(&flags); 397 | assert!(result.tcp); 398 | assert!(result.udp); 399 | 400 | // Test both --tcp and --udp set 401 | let flags = Flags { 402 | tcp: true, 403 | udp: true, 404 | proto: Some("tcp".into()), 405 | ..Default::default() 406 | }; 407 | let result = resolve_protocols(&flags); 408 | assert!(result.tcp); 409 | assert!(result.udp); 410 | } 411 | 412 | #[test] 413 | fn test_generate_completions_subcommand() { 414 | let args = Args::parse_from(["test-bin", "generate-completions", "bash"]); 415 | 416 | match args.command { 417 | Some(Commands::GenerateCompletions { shell }) => { 418 | assert_eq!(shell.to_string(), "bash"); 419 | } 420 | _ => panic!("Expected GenerateCompletions command"), 421 | } 422 | } 423 | 424 | #[test] 425 | fn test_generate_completions_all_shells() { 426 | let shells = ["bash", "zsh", "fish", "elvish"]; 427 | 428 | for shell in &shells { 429 | let args = Args::parse_from(["test-bin", "generate-completions", shell]); 430 | 431 | match args.command { 432 | Some(Commands::GenerateCompletions { 433 | shell: parsed_shell, 434 | }) => { 435 | assert_eq!(parsed_shell.to_string(), *shell); 436 | } 437 | _ => panic!("Expected GenerateCompletions command for {shell}"), 438 | } 439 | } 440 | } 441 | 442 | #[test] 443 | fn test_cli_returns_none_for_subcommands() { 444 | // Mock the Args parsing by directly testing the logic 445 | // This test ensures that when a subcommand is present, cli() returns None 446 | 447 | // We can't easily test the full cli() function without actually running the completion 448 | // generation, so we test the Args parsing logic instead 449 | let args = Args::parse_from(["test-bin", "generate-completions", "bash"]); 450 | 451 | // Verify that a subcommand is present 452 | assert!(args.command.is_some()); 453 | 454 | // Verify that flags are still parsed correctly even with subcommands 455 | assert!(!args.kill); 456 | assert!(args.proto.is_none()); 457 | } 458 | 459 | #[test] 460 | fn test_sort_connections() { 461 | use crate::schemas::Connection; 462 | 463 | fn build_connection( 464 | proto: &str, 465 | local_port: &str, 466 | remote: &str, 467 | remote_port: &str, 468 | program: &str, 469 | pid: &str, 470 | state: &str, 471 | ) -> Connection { 472 | Connection { 473 | proto: proto.to_string(), 474 | local_port: local_port.to_string(), 475 | remote_port: remote_port.to_string(), 476 | ipvx_raw: IpAddr::from_str(remote).unwrap(), 477 | program: program.to_string(), 478 | pid: pid.to_string(), 479 | state: state.to_string(), 480 | remote_address: remote.to_string(), 481 | address_type: AddressType::Extern, 482 | } 483 | } 484 | 485 | let mut connections = vec![ 486 | build_connection("TCP", "443", "9.9.9.9", "443", "nginx", "1", "ESTABLISHED"), 487 | build_connection("UDP", "53", "8.8.8.8", "8080", "apache", "2", "CLOSE_WAIT"), 488 | build_connection("TCP", "80", "0.0.0.0", "80", "postgres", "3", "LISTEN"), 489 | ]; 490 | 491 | // Maps a sort key to the expected order of the connections (represented by their PIDs) after sort 492 | let sort_scenarios = vec![ 493 | (SortField::Pid, ["1", "2", "3"]), 494 | (SortField::RemoteAddress, ["3", "2", "1"]), 495 | (SortField::State, ["2", "1", "3"]), 496 | ]; 497 | 498 | for scenario in sort_scenarios { 499 | sort_connections(&mut connections, scenario.0); 500 | let result_pids: Vec<&str> = connections.iter().map(|c| c.pid.as_str()).collect(); 501 | assert_eq!(result_pids, scenario.1); 502 | } 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /src/connections/common.rs: -------------------------------------------------------------------------------- 1 | use crate::schemas::{AddressType, Connection, FilterOptions}; 2 | 3 | /// Checks if a connection should be filtered out based on options provided by the user. 4 | /// 5 | /// # Arguments 6 | /// * `connection_details`: The connection to check for filtering. 7 | /// * `filter_options`: The filter options provided by the user. 8 | /// 9 | /// # Returns 10 | /// `true` if the connection should be filtered out, `false` if not. 11 | pub fn filter_out_connection( 12 | connection_details: &Connection, 13 | filter_options: &FilterOptions, 14 | ) -> bool { 15 | match &filter_options.by_remote_port { 16 | Some(filter_remote_port) if &connection_details.remote_port != filter_remote_port => { 17 | return true 18 | } 19 | _ => {} 20 | } 21 | match &filter_options.by_local_port { 22 | Some(filter_local_port) if &connection_details.local_port != filter_local_port => { 23 | return true 24 | } 25 | _ => {} 26 | } 27 | match &filter_options.by_remote_address { 28 | Some(filter_remote_address) 29 | if &connection_details.remote_address != filter_remote_address => 30 | { 31 | return true 32 | } 33 | _ => {} 34 | } 35 | match &filter_options.by_program { 36 | Some(filter_program) if &connection_details.program != filter_program => return true, 37 | _ => {} 38 | } 39 | match &filter_options.by_pid { 40 | Some(filter_pid) if &connection_details.pid != filter_pid => return true, 41 | _ => {} 42 | } 43 | if filter_options.by_listen && connection_details.state != "listen" { 44 | return true; 45 | } 46 | if filter_options.by_open && connection_details.state == "close" { 47 | return true; 48 | } 49 | 50 | false 51 | } 52 | 53 | /// Checks if a given IP address is either "unspecified", localhost or an extern address. 54 | /// 55 | /// * `0.0.0.0` or `[::]` -> unspecified 56 | /// * `127.0.0.1` or `[::1]` -> localhost 57 | /// * else -> extern address 58 | /// 59 | /// # Arguments 60 | /// * `remote_address`: The address to be checked. 61 | /// 62 | /// # Returns 63 | /// The address-type as an AddressType enum. 64 | pub fn get_address_type(remote_address: &str) -> AddressType { 65 | if remote_address == "127.0.0.1" || remote_address == "[::1]" || remote_address == "::1" { 66 | return AddressType::Localhost; 67 | } else if remote_address == "0.0.0.0" || remote_address == "[::]" || remote_address == "::" { 68 | return AddressType::Unspecified; 69 | } 70 | AddressType::Extern 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | use std::net::Ipv4Addr; 77 | 78 | #[test] 79 | fn test_get_address_type() { 80 | use crate::schemas::AddressType; 81 | 82 | assert_eq!(get_address_type("127.0.0.1"), AddressType::Localhost); 83 | assert_eq!(get_address_type("[::1]"), AddressType::Localhost); 84 | assert_eq!(get_address_type("0.0.0.0"), AddressType::Unspecified); 85 | assert_eq!(get_address_type("[::]"), AddressType::Unspecified); 86 | assert_eq!(get_address_type("8.8.8.8"), AddressType::Extern); 87 | } 88 | 89 | #[test] 90 | fn test_filter_out_connection_by_port() { 91 | use crate::schemas::{AddressType, Connection, FilterOptions}; 92 | 93 | let conn = Connection { 94 | proto: "tcp".to_string(), 95 | local_port: "8080".to_string(), 96 | remote_port: "443".to_string(), 97 | remote_address: "8.8.8.8".to_string(), 98 | program: "nginx".to_string(), 99 | pid: "123".to_string(), 100 | state: "established".to_string(), 101 | address_type: AddressType::Extern, 102 | ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(), 103 | }; 104 | 105 | let filter_by_matching_port = FilterOptions { 106 | by_local_port: Some("8080".to_string()), 107 | ..Default::default() 108 | }; 109 | assert!(!filter_out_connection(&conn, &filter_by_matching_port)); 110 | 111 | let filter_by_non_matching_port = FilterOptions { 112 | by_local_port: Some("8181".to_string()), 113 | ..Default::default() 114 | }; 115 | assert!(filter_out_connection(&conn, &filter_by_non_matching_port)); 116 | } 117 | 118 | #[test] 119 | fn test_filter_out_connection_by_state() { 120 | use crate::schemas::{AddressType, Connection, FilterOptions}; 121 | 122 | let mut conn = Connection { 123 | proto: "udp".to_string(), 124 | local_port: "8080".to_string(), 125 | remote_port: "443".to_string(), 126 | remote_address: "8.8.8.8".to_string(), 127 | program: "nginx".to_string(), 128 | pid: "123".to_string(), 129 | state: "close".to_string(), 130 | address_type: AddressType::Extern, 131 | ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(), 132 | }; 133 | 134 | let filter_by_open_state = FilterOptions { 135 | by_open: true, 136 | ..Default::default() 137 | }; 138 | assert!(filter_out_connection(&conn, &filter_by_open_state)); 139 | 140 | let no_active_open_filter = FilterOptions { 141 | by_open: false, 142 | ..Default::default() 143 | }; 144 | assert!(!filter_out_connection(&conn, &no_active_open_filter)); 145 | 146 | conn.state = "listen".to_string(); 147 | 148 | let filter_by_listen_state = FilterOptions { 149 | by_listen: true, 150 | ..Default::default() 151 | }; 152 | assert!(!filter_out_connection(&conn, &filter_by_listen_state)); 153 | 154 | let no_active_listen_filter = FilterOptions { 155 | by_listen: false, 156 | ..Default::default() 157 | }; 158 | assert!(!filter_out_connection(&conn, &no_active_listen_filter)); 159 | } 160 | 161 | #[test] 162 | fn test_filter_out_connection_by_pid_and_program() { 163 | use crate::schemas::{AddressType, Connection, FilterOptions}; 164 | 165 | let conn = Connection { 166 | proto: "tcp".to_string(), 167 | local_port: "8080".to_string(), 168 | remote_port: "443".to_string(), 169 | remote_address: "8.8.8.8".to_string(), 170 | program: "nginx".to_string(), 171 | pid: "123".to_string(), 172 | state: "close".to_string(), 173 | address_type: AddressType::Extern, 174 | ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(), 175 | }; 176 | 177 | let filter_by_open_state = FilterOptions { 178 | by_pid: Some("123".to_string()), 179 | ..Default::default() 180 | }; 181 | assert!(!filter_out_connection(&conn, &filter_by_open_state)); 182 | 183 | let no_active_open_filter = FilterOptions { 184 | by_program: Some("postgres".to_string()), 185 | ..Default::default() 186 | }; 187 | assert!(filter_out_connection(&conn, &no_active_open_filter)); 188 | } 189 | 190 | #[test] 191 | fn test_filter_out_connection_by_multiple_conditions() { 192 | use crate::schemas::{AddressType, Connection, FilterOptions}; 193 | 194 | let mut conn = Connection { 195 | proto: "tcp".to_string(), 196 | local_port: "8080".to_string(), 197 | remote_port: "443".to_string(), 198 | remote_address: "8.8.8.8".to_string(), 199 | program: "python".to_string(), 200 | pid: "123".to_string(), 201 | state: "listen".to_string(), 202 | address_type: AddressType::Extern, 203 | ipvx_raw: Ipv4Addr::new(8, 8, 8, 8).into(), 204 | }; 205 | 206 | let filter_by_multiple_conditions = FilterOptions { 207 | by_local_port: Some("8080".to_string()), 208 | by_pid: Some("123".to_string()), 209 | by_program: Some("python".to_string()), 210 | by_listen: true, 211 | ..Default::default() 212 | }; 213 | assert!(!filter_out_connection( 214 | &conn, 215 | &filter_by_multiple_conditions 216 | )); 217 | 218 | conn.state = "close".to_string(); 219 | assert!(filter_out_connection(&conn, &filter_by_multiple_conditions)); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/connections/linux.rs: -------------------------------------------------------------------------------- 1 | use crate::connections::common::{filter_out_connection, get_address_type}; 2 | use crate::schemas::{Connection, FilterOptions}; 3 | use procfs::process::FDTarget; 4 | use procfs::process::Stat; 5 | use std::collections::HashMap; 6 | use std::net::SocketAddr; 7 | 8 | /// General struct type for TCP and UDP entries. 9 | #[derive(Debug)] 10 | pub struct NetEntry { 11 | pub protocol: String, 12 | pub local_address: SocketAddr, 13 | pub remote_address: SocketAddr, 14 | pub state: String, 15 | pub inode: u64, 16 | } 17 | 18 | /// Splits a string combined of an IP address and port with a ":" delimiter into two parts. 19 | /// 20 | /// # Arguments 21 | /// * `address`: The combination of address and port joined by a ":", e.g. "127.0.0.1:5432" 22 | /// 23 | /// # Example 24 | /// ``` 25 | /// let address_port_1 = "127.0.0.1:5432".to_string(); 26 | /// assert_eq!(split_address(address_port_1), Some(("5432", "127.0.0.1"))); 27 | /// 28 | /// let address_port_2 = "fails.com".to_string(); 29 | /// assert_eq!(split_address(address_port_2), None); 30 | /// ``` 31 | /// 32 | /// # Returns 33 | /// If the string can be successfully split, 34 | /// it will return a tuple containing the address and the port, if not `None`. 35 | fn split_address(address: &str) -> Option<(&str, &str)> { 36 | static DELIMITER: &str = ":"; 37 | 38 | let mut address_parts = address.rsplitn(2, DELIMITER); 39 | match (address_parts.next(), address_parts.next()) { 40 | (Some(first), Some(second)) => Some((second, first)), 41 | _ => None, 42 | } 43 | } 44 | 45 | /// Handles the output of the `split_address` function by replacing the port with a "-" if the string couldn't be split. 46 | /// ###### TODO: maybe combine it with the `split_address` function. 47 | /// 48 | /// # Arguments 49 | /// * `address`: The address-port combination which should be split. 50 | /// 51 | /// # Example 52 | /// ``` 53 | /// let address_port_1 = "127.0.0.1:5432".to_string(); 54 | /// assert_eq!(get_address_parts(address_port_1), ("5432", "127.0.0.1")); 55 | /// 56 | /// let address_port_2 = "fails.com".to_string(); 57 | /// assert_eq!(get_address_parts(address_port_1), ("-", "127.0.0.1")); 58 | /// ``` 59 | /// 60 | /// # Returns 61 | /// A tuple containing the address and port or just the address and a "-" if there wasn't a port. 62 | fn get_address_parts(address: &str) -> (String, String) { 63 | split_address(address) 64 | .map(|(a, p)| (a.to_string(), p.to_string())) 65 | .unwrap_or((address.to_string(), "-".to_string())) 66 | } 67 | 68 | /// Gets all running processes on the system using the "procfs" crate. 69 | /// This code is taken from the "procfs" crate documentation. 70 | /// 71 | /// # Arguments 72 | /// None 73 | /// 74 | /// # Returns 75 | /// A map of all current processes. 76 | fn get_processes() -> HashMap<u64, Stat> { 77 | let all_procs = procfs::process::all_processes().unwrap(); 78 | 79 | let mut map: HashMap<u64, Stat> = HashMap::new(); 80 | for p in all_procs { 81 | let process = p.unwrap(); 82 | if let (Ok(stat), Ok(fds)) = (process.stat(), process.fd()) { 83 | for fd in fds { 84 | if let FDTarget::Socket(inode) = fd.unwrap().target { 85 | map.insert(inode, stat.clone()); 86 | } 87 | } 88 | } 89 | } 90 | map 91 | } 92 | 93 | fn get_connection_data(net_entry: NetEntry, all_processes: &HashMap<u64, Stat>) -> Connection { 94 | let local_address_full = format!("{}", net_entry.local_address); 95 | let (_, local_port) = get_address_parts(&local_address_full); 96 | 97 | let remote_address_full = format!("{}", net_entry.remote_address); 98 | let (remote_address, remote_port) = get_address_parts(&remote_address_full); 99 | let state = net_entry.state; 100 | 101 | let (program, pid) = all_processes 102 | .get(&net_entry.inode) 103 | .map(|stat| (stat.comm.to_string(), stat.pid.to_string())) 104 | .unwrap_or(("-".to_string(), "-".to_string())); 105 | 106 | let address_type = get_address_type(&remote_address); 107 | 108 | let connection: Connection = Connection { 109 | proto: net_entry.protocol, 110 | local_port, 111 | remote_address, 112 | remote_port, 113 | program, 114 | pid, 115 | state, 116 | address_type, 117 | ipvx_raw: net_entry.remote_address.ip(), 118 | }; 119 | 120 | connection 121 | } 122 | 123 | /// Gets all currently open TCP connections using the "procfs" crate and processes them. 124 | /// 125 | /// # Arguments 126 | /// * `all_processes`: A map of all running processes on the system. 127 | /// * `filter_options`: The filter options provided by the user. 128 | /// 129 | /// # Returns 130 | /// All processed and filtered TCP connections as a `Connection` struct in a vector. 131 | fn get_tcp_connections( 132 | all_processes: &HashMap<u64, Stat>, 133 | filter_options: &FilterOptions, 134 | ) -> Vec<Connection> { 135 | let mut tcp_entries = procfs::net::tcp().unwrap(); 136 | if !filter_options.exclude_ipv6 { 137 | tcp_entries.extend(procfs::net::tcp6().unwrap()); 138 | } 139 | 140 | tcp_entries 141 | .iter() 142 | .filter_map(|entry| { 143 | let tcp_entry: NetEntry = NetEntry { 144 | protocol: "tcp".to_string(), 145 | local_address: entry.local_address, 146 | remote_address: entry.remote_address, 147 | state: format!("{:?}", entry.state).to_ascii_lowercase(), 148 | inode: entry.inode, 149 | }; 150 | let connection = get_connection_data(tcp_entry, all_processes); 151 | 152 | let filter_connection: bool = filter_out_connection(&connection, filter_options); 153 | if !filter_connection { 154 | Some(connection) 155 | } else { 156 | None 157 | } 158 | }) 159 | .collect() 160 | } 161 | 162 | /// Gets all currently open UDP connections using the "procfs" crate and processes them. 163 | /// 164 | /// # Arguments 165 | /// * `all_processes`: A map of all running processes on the system. 166 | /// * `filter_options`: The filter options provided by the user. 167 | /// 168 | /// # Returns 169 | /// All processed and filtered UDP connections as a `Connection` struct in a vector. 170 | fn get_udp_connections( 171 | all_processes: &HashMap<u64, Stat>, 172 | filter_options: &FilterOptions, 173 | ) -> Vec<Connection> { 174 | let mut udp_entries = procfs::net::udp().unwrap(); 175 | if !filter_options.exclude_ipv6 { 176 | udp_entries.extend(procfs::net::udp6().unwrap()); 177 | } 178 | 179 | udp_entries 180 | .iter() 181 | .filter_map(|entry| { 182 | let udp_entry: NetEntry = NetEntry { 183 | protocol: "udp".to_string(), 184 | local_address: entry.local_address, 185 | remote_address: entry.remote_address, 186 | state: format!("{:?}", entry.state).to_ascii_lowercase(), 187 | inode: entry.inode, 188 | }; 189 | let connection: Connection = get_connection_data(udp_entry, all_processes); 190 | 191 | let filter_connection: bool = filter_out_connection(&connection, filter_options); 192 | if !filter_connection { 193 | Some(connection) 194 | } else { 195 | None 196 | } 197 | }) 198 | .collect() 199 | } 200 | 201 | /// Gets both TCP and UDP connections and combines them based on protocol filter options. 202 | /// 203 | /// # Arguments 204 | /// * `filter_options`: The filter options provided by the user. 205 | /// 206 | /// # Returns 207 | /// All processed and filtered TCP/UDP connections as a `Connection` struct in a vector. 208 | pub fn get_connections(filter_options: &FilterOptions) -> Vec<Connection> { 209 | let all_processes = get_processes(); 210 | 211 | let mut connections = Vec::new(); 212 | if filter_options.by_proto.tcp { 213 | connections.extend(get_tcp_connections(&all_processes, filter_options)) 214 | } 215 | if filter_options.by_proto.udp { 216 | connections.extend(get_udp_connections(&all_processes, filter_options)) 217 | } 218 | 219 | connections 220 | } 221 | 222 | #[cfg(test)] 223 | mod tests { 224 | use super::*; 225 | 226 | #[test] 227 | fn test_split_address_valid() { 228 | let addr = "127.0.0.1:5432"; 229 | assert_eq!(split_address(addr), Some(("127.0.0.1", "5432"))); 230 | 231 | let addr = "[::1]:8080"; 232 | assert_eq!(split_address(addr), Some(("[::1]", "8080"))); 233 | } 234 | 235 | #[test] 236 | fn test_split_address_invalid() { 237 | let addr = "localhost"; 238 | assert_eq!(split_address(addr), None); 239 | let addr = "192.168.0.1"; 240 | assert_eq!(split_address(addr), None); 241 | } 242 | 243 | #[test] 244 | fn test_get_address_parts_valid() { 245 | let addr = "192.168.0.1:80"; 246 | let (address, port) = get_address_parts(addr); 247 | assert_eq!(address, "192.168.0.1"); 248 | assert_eq!(port, "80"); 249 | } 250 | 251 | #[test] 252 | fn test_get_address_parts_invalid() { 253 | let addr = "example.com"; 254 | let (address, port) = get_address_parts(addr); 255 | assert_eq!(address, "example.com"); 256 | assert_eq!(port, "-"); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/connections/macos.rs: -------------------------------------------------------------------------------- 1 | use crate::connections::common::{filter_out_connection, get_address_type}; 2 | use crate::schemas::{Connection, FilterOptions}; 3 | use libproc::libproc::proc_pid; 4 | use netstat2::{ 5 | get_sockets_info, AddressFamilyFlags, ProtocolFlags, ProtocolSocketInfo as NetstatSocketInfo, 6 | SocketInfo, 7 | }; 8 | use std::collections::HashSet; 9 | 10 | /// Retrieves the name of a process given its PID on macOS using the libproc library. 11 | /// 12 | /// # Arguments 13 | /// * `pid`: The process ID for which to obtain the process name. 14 | /// 15 | /// # Returns 16 | /// A string containing the process name if found, or "-" if the name cannot be retrieved. 17 | fn get_process_name(pid: i32) -> String { 18 | match proc_pid::name(pid) { 19 | Ok(name) => name, 20 | Err(_) => "-".to_string(), 21 | } 22 | } 23 | 24 | /// Parses and filters TCP and/or UDP connections using socket information. 25 | /// 26 | /// # Arguments 27 | /// * `sockets_info`: List of socket information coming from the netstat2 crate 28 | /// * `filter_options`: The filter options provided by the user. 29 | /// 30 | /// # Returns 31 | /// All filtered TCP/UDP connections as a `Connection` struct in a vector. 32 | fn parse_connections( 33 | sockets_info: &[SocketInfo], 34 | filter_options: &FilterOptions, 35 | ) -> Vec<Connection> { 36 | let mut af_flags = AddressFamilyFlags::empty(); 37 | if !filter_options.exclude_ipv6 { 38 | af_flags |= AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6; 39 | } else { 40 | af_flags |= AddressFamilyFlags::IPV4; 41 | } 42 | 43 | let mut proto_flags = ProtocolFlags::empty(); 44 | if filter_options.by_proto.tcp { 45 | proto_flags |= ProtocolFlags::TCP; 46 | } 47 | if filter_options.by_proto.udp { 48 | proto_flags |= ProtocolFlags::UDP; 49 | } 50 | 51 | // Temporary storage for connections, for deduplication 52 | let mut seen_connections = HashSet::new(); 53 | 54 | // Convert the socket information to our Connection type 55 | sockets_info 56 | .iter() 57 | .filter_map(|si| { 58 | let (proto, local_port, remote_address, remote_port, state) = 59 | match &si.protocol_socket_info { 60 | NetstatSocketInfo::Tcp(tcp_si) => { 61 | let state = format!("{}", tcp_si.state).to_ascii_lowercase(); 62 | ( 63 | "tcp".to_string(), 64 | tcp_si.local_port.to_string(), 65 | tcp_si.remote_addr.to_string(), 66 | tcp_si.remote_port.to_string(), 67 | state, 68 | ) 69 | } 70 | NetstatSocketInfo::Udp(udp_si) => ( 71 | "udp".to_string(), 72 | udp_si.local_port.to_string(), 73 | "0.0.0.0".to_string(), 74 | "-".to_string(), 75 | "-".to_string(), 76 | ), 77 | }; 78 | 79 | let (program, pid) = if let Some(first_pid) = si.associated_pids.first() { 80 | let proc_name = get_process_name(*first_pid as i32); 81 | (proc_name, first_pid.to_string()) 82 | } else { 83 | ("-".to_string(), "-".to_string()) 84 | }; 85 | 86 | // Create a unique key for deduplication 87 | let connection_key = 88 | format!("{proto}:{local_port}:{remote_address}:{remote_port}:{state}:{pid}"); 89 | 90 | // If the connection has already been processed, skip it 91 | if !seen_connections.insert(connection_key) { 92 | return None; 93 | } 94 | 95 | let conn = Connection { 96 | proto, 97 | local_port, 98 | remote_address: remote_address.clone(), 99 | remote_port, 100 | program, 101 | pid, 102 | state, 103 | address_type: get_address_type(&remote_address), 104 | ipvx_raw: si.local_addr(), 105 | }; 106 | 107 | if filter_out_connection(&conn, filter_options) { 108 | None 109 | } else { 110 | Some(conn) 111 | } 112 | }) 113 | .collect() 114 | } 115 | 116 | /// Gets and filters TCP and/or UDP connections using socket information from the netstat2 crate. 117 | /// 118 | /// # Arguments 119 | /// * `filter_options`: The filter options provided by the user. 120 | /// 121 | /// # Returns 122 | /// All processed and filtered TCP/UDP connections as a `Connection` struct in a vector. 123 | pub fn get_connections(filter_options: &FilterOptions) -> Vec<Connection> { 124 | let mut af_flags = AddressFamilyFlags::empty(); 125 | if !filter_options.exclude_ipv6 { 126 | af_flags |= AddressFamilyFlags::IPV4 | AddressFamilyFlags::IPV6; 127 | } else { 128 | af_flags |= AddressFamilyFlags::IPV4; 129 | } 130 | 131 | let mut proto_flags = ProtocolFlags::empty(); 132 | if filter_options.by_proto.tcp { 133 | proto_flags |= ProtocolFlags::TCP; 134 | } 135 | if filter_options.by_proto.udp { 136 | proto_flags |= ProtocolFlags::UDP; 137 | } 138 | 139 | let sockets_info = match get_sockets_info(af_flags, proto_flags) { 140 | Ok(sockets) => sockets, 141 | Err(_) => return Vec::new(), 142 | }; 143 | 144 | parse_connections(&sockets_info, filter_options) 145 | } 146 | 147 | #[cfg(test)] 148 | mod tests { 149 | use super::*; 150 | use crate::schemas::Protocols; 151 | use netstat2::{ProtocolSocketInfo, SocketInfo, TcpSocketInfo, TcpState}; 152 | use std::net::{IpAddr, Ipv4Addr}; 153 | 154 | #[test] 155 | fn test_parse_connections_tcp() { 156 | let mock_socket = SocketInfo { 157 | protocol_socket_info: ProtocolSocketInfo::Tcp(TcpSocketInfo { 158 | local_port: 8080, 159 | remote_port: 443, 160 | local_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 161 | remote_addr: IpAddr::V4(Ipv4Addr::new(93, 184, 216, 34)), 162 | state: TcpState::Established, 163 | }), 164 | associated_pids: vec![1234], 165 | }; 166 | 167 | let filter_options = FilterOptions { 168 | exclude_ipv6: false, 169 | by_proto: Protocols { 170 | tcp: true, 171 | udp: false, 172 | }, 173 | ..Default::default() 174 | }; 175 | 176 | let connections = parse_connections(&vec![mock_socket], &filter_options); 177 | 178 | assert_eq!(connections.len(), 1); 179 | let conn = &connections[0]; 180 | assert_eq!(conn.proto, "tcp"); 181 | assert_eq!(conn.local_port, "8080"); 182 | assert_eq!(conn.remote_port, "443"); 183 | assert_eq!(conn.remote_address, "93.184.216.34"); 184 | assert_eq!(conn.state, "established"); 185 | assert_eq!(conn.pid, "1234"); 186 | } 187 | 188 | #[test] 189 | fn test_parse_connections_udp() { 190 | let mock_socket = SocketInfo { 191 | protocol_socket_info: ProtocolSocketInfo::Udp(netstat2::UdpSocketInfo { 192 | local_port: 53, 193 | local_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 194 | }), 195 | associated_pids: vec![5678], 196 | }; 197 | 198 | let filter_options = FilterOptions { 199 | by_proto: Protocols { 200 | tcp: false, 201 | udp: true, 202 | }, 203 | ..Default::default() 204 | }; 205 | 206 | let connections = parse_connections(&vec![mock_socket], &filter_options); 207 | 208 | assert_eq!(connections.len(), 1); 209 | let conn = &connections[0]; 210 | assert_eq!(conn.proto, "udp"); 211 | assert_eq!(conn.local_port, "53"); 212 | assert_eq!(conn.remote_port, "-"); 213 | assert_eq!(conn.remote_address, "0.0.0.0"); 214 | assert_eq!(conn.state, "-"); 215 | assert_eq!(conn.pid, "5678"); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/connections/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod common; 2 | 3 | #[cfg(target_os = "linux")] 4 | mod linux; 5 | #[cfg(target_os = "macos")] 6 | mod macos; 7 | 8 | use crate::schemas::Connection; 9 | use crate::schemas::FilterOptions; 10 | 11 | /// Retrieves all TCP/UDP network connections based on the current operating system (Linux or macOS). 12 | /// 13 | /// # Arguments 14 | /// * `filter_options`: The filter options provided by the user. 15 | /// 16 | /// # Returns 17 | /// All processed and filtered TCP/UDP connections as a `Connection` struct in a vector. 18 | pub fn get_all_connections(filter_options: &FilterOptions) -> Vec<Connection> { 19 | #[cfg(target_os = "linux")] 20 | { 21 | linux::get_connections(filter_options) 22 | } 23 | 24 | #[cfg(target_os = "macos")] 25 | { 26 | macos::get_connections(filter_options) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Pipe-safe stdout printing -- alternative to print!. 2 | /// This macro exists because there is no way to handle pipes ending abruptly with print! 3 | /// For more information, read: 4 | /// - https://users.rust-lang.org/t/why-does-the-pipe-cause-the-panic-in-the-standard-library/107222/4 5 | /// - rust-lang/rust#97889 6 | /// 7 | /// # Usage 8 | /// Same arguments & expected behaviour as print!, with graceful handling of (expected) IO errors. 9 | #[macro_export] 10 | macro_rules! sout { 11 | ($($arg:tt)*) => {{ 12 | use std::io::Write; 13 | 14 | match write!(std::io::stdout(), $($arg)*) { 15 | Ok(_) => (), 16 | Err(broken_pipe) if broken_pipe.kind() == std::io::ErrorKind::BrokenPipe => (), 17 | Err(err) => panic!("Unknown error occurred while writing to stdout {:?}", err.kind()), 18 | } 19 | }}; 20 | } 21 | 22 | /// Pipe-safe stdout printing -- alternative to println!. 23 | /// This macro exists because there is no way to handle pipes ending abruptly with println! 24 | /// For more information, read: 25 | /// - https://users.rust-lang.org/t/why-does-the-pipe-cause-the-panic-in-the-standard-library/107222/4 26 | /// - rust-lang/rust#97889 27 | /// 28 | /// # Usage 29 | /// Same arguments & expected behaviour as println!, with graceful handling of (expected) IO errors. 30 | #[macro_export] 31 | macro_rules! soutln { 32 | () => {{ 33 | $crate::sout!("\n"); 34 | }}; 35 | ($($arg:tt)*) => {{ 36 | $crate::sout!("{}\n", format!($($arg)*)); 37 | }}; 38 | } 39 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod connections; 3 | mod macros; 4 | mod schemas; 5 | mod table; 6 | mod utils; 7 | 8 | use clap::CommandFactory; 9 | use cli::{print_completions, Args, CliCommand, Commands}; 10 | use schemas::{Connection, FilterOptions}; 11 | 12 | use crate::cli::sort_connections; 13 | 14 | fn main() { 15 | let args = match cli::cli() { 16 | CliCommand::Subcommand(Commands::GenerateCompletions { shell }) => { 17 | let mut cmd = Args::command(); 18 | print_completions(shell, &mut cmd); 19 | return; 20 | } 21 | CliCommand::Run(flags) => flags, 22 | }; 23 | 24 | let filter_options: FilterOptions = FilterOptions { 25 | by_proto: cli::resolve_protocols(&args), 26 | by_remote_address: args.ip, 27 | by_remote_port: args.remote_port, 28 | by_local_port: args.port, 29 | by_program: args.program, 30 | by_pid: args.pid, 31 | by_open: args.open, 32 | by_listen: args.listen, 33 | exclude_ipv6: args.exclude_ipv6, 34 | }; 35 | 36 | let mut all_connections: Vec<Connection> = connections::get_all_connections(&filter_options); 37 | 38 | if let Some(sort) = args.sort { 39 | sort_connections(&mut all_connections, sort); 40 | } 41 | 42 | if args.reverse { 43 | all_connections.reverse(); 44 | } 45 | 46 | if args.json { 47 | let result = table::get_connections_json(&all_connections); 48 | soutln!("{}", result); 49 | } else if args.format.is_some() { 50 | let result = table::get_connections_formatted(&all_connections, &args.format.unwrap()); 51 | soutln!("{}", result); 52 | } else { 53 | table::print_connections_table(&all_connections, args.compact); 54 | } 55 | 56 | if args.kill { 57 | cli::interactive_process_kill(&all_connections); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/schemas.rs: -------------------------------------------------------------------------------- 1 | use std::net::IpAddr; 2 | 3 | /// Represents the type of an IP address. 4 | /// 5 | /// # Variants 6 | /// * `Localhost`: Represents the localhost/127.0.0.1 address. 7 | /// * `Unspecified`: Represents an unspecified or wildcard address. 8 | /// * `Extern`: Represents an external address. 9 | #[derive(Debug, PartialEq, serde::Serialize)] 10 | pub enum AddressType { 11 | Localhost, 12 | Unspecified, 13 | Extern, 14 | } 15 | 16 | /// Contains which type(s) of protocols the user wants to see. 17 | #[derive(Debug, Default)] 18 | pub struct Protocols { 19 | pub tcp: bool, 20 | pub udp: bool, 21 | } 22 | 23 | /// Represents a processed socket connection with all its attributes. 24 | #[derive(Debug, serde::Serialize)] 25 | #[serde(rename_all = "snake_case")] 26 | pub struct Connection { 27 | pub proto: String, 28 | pub local_port: String, 29 | pub remote_address: String, 30 | pub remote_port: String, 31 | pub program: String, 32 | pub pid: String, 33 | pub state: String, 34 | pub address_type: AddressType, 35 | 36 | /// Internal variable used only for ordering operations of raw ipv4/6 addresses 37 | #[serde(skip_serializing)] 38 | pub ipvx_raw: IpAddr, 39 | } 40 | 41 | /// Contains options for filtering a `Connection`. 42 | #[derive(Debug, Default)] 43 | pub struct FilterOptions { 44 | pub by_proto: Protocols, 45 | pub by_program: Option<String>, 46 | pub by_pid: Option<String>, 47 | pub by_remote_address: Option<String>, 48 | pub by_remote_port: Option<String>, 49 | pub by_local_port: Option<String>, 50 | pub by_open: bool, 51 | pub by_listen: bool, 52 | pub exclude_ipv6: bool, 53 | } 54 | 55 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 56 | pub enum Protocol { 57 | Tcp, 58 | Udp, 59 | } 60 | 61 | impl std::str::FromStr for Protocol { 62 | type Err = (); 63 | 64 | fn from_str(input: &str) -> Result<Self, Self::Err> { 65 | match input.to_lowercase().as_str() { 66 | "tcp" => Ok(Protocol::Tcp), 67 | "udp" => Ok(Protocol::Udp), 68 | _ => Err(()), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/table.rs: -------------------------------------------------------------------------------- 1 | use handlebars::{Handlebars, RenderErrorReason}; 2 | use termimad::crossterm::style::{Attribute::*, Color::*}; 3 | use termimad::*; 4 | 5 | use crate::schemas::{AddressType, Connection}; 6 | use crate::utils::pretty_print_syntax_error; 7 | use crate::{soutln, utils}; 8 | 9 | /// Uses the termimad crate to create a custom appearance for Markdown text in the console. 10 | /// 11 | /// # Appearance 12 | /// * **bold** text -> bold and cyan 13 | /// * *italic* text -> italic and light gray 14 | /// * ~~strikeout~~ text -> not struck out, red and blinking 15 | /// * `inline code` text -> not code formatted, yellow 16 | /// 17 | /// # Arguments 18 | /// None 19 | /// 20 | /// # Returns 21 | /// A custom markdown "skin". 22 | fn create_table_style(use_compact_mode: bool) -> MadSkin { 23 | let mut skin = MadSkin::default(); 24 | skin.bold.set_fg(Cyan); 25 | skin.italic.set_fg(gray(11)); 26 | skin.strikeout = CompoundStyle::new(Some(Red), None, RapidBlink.into()); 27 | skin.paragraph.align = Alignment::Left; 28 | skin.table.align = if use_compact_mode { 29 | Alignment::Left 30 | } else { 31 | Alignment::Center 32 | }; 33 | skin.inline_code = CompoundStyle::new(Some(Yellow), None, Encircled.into()); 34 | 35 | skin 36 | } 37 | 38 | /// Marks localhost and unspecified IP addresses (i.e., 0.0.0.0) using Markdown formatting 39 | /// 40 | /// * `address_type` == Localhost -> *italic* + "localhost" 41 | /// * `address_type` == Unspecified -> *italic* 42 | /// * `address_type` == Extern -> not formatted 43 | /// 44 | /// # Arguments 45 | /// * `remote_address`: The remote address. 46 | /// * `address_type`: The address type as an AddressType enum. 47 | /// 48 | /// # Example 49 | /// ``` 50 | /// let address = "127.0.0.1".to_string(); 51 | /// let address_type = AddressType::Localhost; 52 | /// let formatted = format_known_address(&address, &address_type); 53 | /// assert_eq!(formatted, "*127.0.0.1 localhost*"); 54 | /// ``` 55 | /// 56 | /// # Returns 57 | /// A Markdown formatted string based on the address-type. 58 | fn format_known_address(remote_address: &String, address_type: &AddressType) -> String { 59 | match address_type { 60 | AddressType::Unspecified => { 61 | format!("*{remote_address}*") 62 | } 63 | AddressType::Localhost => { 64 | format!("*{remote_address} localhost*") 65 | } 66 | AddressType::Extern => remote_address.to_string(), 67 | } 68 | } 69 | 70 | /// Creates a Markdown table row with just empty characters with the width of the terminal window. 71 | /// 72 | /// # Argument 73 | /// * `terminal_width`: The current width of the terminal. 74 | /// * `max_column_spaces`: An array in which the values represent the max-width of each of the 7 Markdown table rows. 75 | /// 76 | /// # Returns 77 | /// A Markdown table row string in which each column is filled with as many empty characters needed to fit in content and as well fill out the terminal width. 78 | fn fill_terminal_width(terminal_width: u16, max_column_spaces: [u16; 7]) -> String { 79 | let total_column_spaces: u16 = max_column_spaces.iter().sum(); 80 | 81 | let calculate_column_width = |column_space: u16| { 82 | (column_space as f64 / total_column_spaces as f64) * (terminal_width as f64) 83 | }; 84 | let empty_character = "\u{2800}"; 85 | 86 | let mut row: String = String::new(); 87 | for &max_column_space in &max_column_spaces { 88 | row.push_str(&format!( 89 | "| {} ", 90 | empty_character.repeat(calculate_column_width(max_column_space) as usize) 91 | )); 92 | } 93 | row.push_str("|\n"); 94 | 95 | row 96 | } 97 | 98 | /// Prints all current connections in a pretty Markdown table. 99 | /// 100 | /// # Arguments 101 | /// * `all_connections`: A list containing all current connections as a `Connection` struct. 102 | /// 103 | /// # Returns 104 | /// None 105 | pub fn print_connections_table(all_connections: &[Connection], use_compact_mode: bool) { 106 | let skin: MadSkin = create_table_style(use_compact_mode); 107 | let (terminal_width, _) = terminal_size(); 108 | 109 | // Add table headers 110 | static CENTER_MARKDOWN_ROW: &str = "| :-: | :-: | :-: | :-: | :-: | :-: | :-: |\n"; 111 | let mut markdown = CENTER_MARKDOWN_ROW.to_string(); 112 | markdown.push_str("| **#** | **proto** | **local port** | **remote address** | **remote port** | **pid** *program* | **state** |\n"); 113 | markdown.push_str(CENTER_MARKDOWN_ROW); 114 | 115 | for (idx, connection) in all_connections.iter().enumerate() { 116 | let formatted_remote_address: String = 117 | format_known_address(&connection.remote_address, &connection.address_type); 118 | 119 | markdown.push_str(&format!( 120 | "| *{}* | {} | {} | {} | {} | {} *{}* | {} |\n", 121 | idx + 1, 122 | connection.proto, 123 | connection.local_port, 124 | &formatted_remote_address, 125 | connection.remote_port, 126 | connection.pid, 127 | connection.program, 128 | connection.state 129 | )); 130 | 131 | if !use_compact_mode && idx < all_connections.len() - 1 { 132 | markdown.push_str(CENTER_MARKDOWN_ROW); 133 | } 134 | } 135 | 136 | if !use_compact_mode { 137 | // Create an empty row that forces the table to fit the terminal with respect to how much space ... 138 | // ... each column should receive based on the max length of each column (in the array below) 139 | let max_column_spaces: [u16; 7] = [5, 8, 8, 28, 7, 24, 13]; 140 | let terminal_filling_row: String = fill_terminal_width(terminal_width, max_column_spaces); 141 | markdown.push_str(&terminal_filling_row); 142 | } 143 | 144 | markdown.push_str(CENTER_MARKDOWN_ROW); 145 | 146 | soutln!("{}", skin.term_text(&markdown)); 147 | utils::pretty_print_info(&format!("{} Connections", all_connections.len())) 148 | } 149 | 150 | /// Prints all current connections in a json format. 151 | /// 152 | /// # Arguments 153 | /// * `all_connections`: A list containing all current connections as a `Connection` struct. 154 | /// 155 | /// # Returns 156 | /// None 157 | pub fn get_connections_json(all_connections: &Vec<Connection>) -> String { 158 | serde_json::to_string_pretty(all_connections).unwrap() 159 | } 160 | 161 | /// Prints all current connections in a custom format. 162 | /// 163 | /// # Arguments 164 | /// * `all_connections`: A list containing all current connections as a `Connection` struct. 165 | /// * `template_string`: A string template format for an output 166 | /// 167 | /// # Returns 168 | /// None 169 | pub fn get_connections_formatted( 170 | all_connections: &Vec<Connection>, 171 | template_string: &String, 172 | ) -> String { 173 | let mut registry = Handlebars::new(); 174 | registry.set_strict_mode(true); 175 | 176 | if let Err(err) = registry.register_template_string("connection_template", template_string) { 177 | let (line_no, column_no) = err.pos().unwrap_or((1, 1)); 178 | 179 | pretty_print_syntax_error( 180 | "Invalid template syntax.", 181 | template_string, 182 | line_no, 183 | column_no, 184 | ); 185 | std::process::exit(2); 186 | } 187 | 188 | let mut rendered_lines = Vec::new(); 189 | 190 | for connection in all_connections { 191 | let json_value = serde_json::to_value(connection).unwrap(); 192 | let rendered_line = registry.render("connection_template", &json_value); 193 | 194 | if let Err(err) = rendered_line { 195 | let (line_no, column_no) = (err.line_no.unwrap_or(1), err.column_no.unwrap_or(1)); 196 | 197 | match err.reason() { 198 | RenderErrorReason::MissingVariable(Some(var_name)) => { 199 | pretty_print_syntax_error( 200 | &format!("Invalid template variable '{var_name}'."), 201 | template_string, 202 | line_no, 203 | column_no, 204 | ); 205 | } 206 | _ => { 207 | pretty_print_syntax_error( 208 | &format!("Template error - {}", err.reason()), 209 | template_string, 210 | line_no, 211 | column_no, 212 | ); 213 | } 214 | } 215 | std::process::exit(2); 216 | } 217 | 218 | rendered_lines.push(rendered_line.unwrap()); 219 | } 220 | 221 | rendered_lines.join("\n") 222 | } 223 | 224 | #[cfg(test)] 225 | mod tests { 226 | use std::net::{Ipv4Addr, Ipv6Addr}; 227 | 228 | use super::*; 229 | 230 | #[test] 231 | fn test_format_known_address_localhost() { 232 | let addr = "127.0.0.1".to_string(); 233 | let result = format_known_address(&addr, &AddressType::Localhost); 234 | assert_eq!(result, "*127.0.0.1 localhost*"); 235 | } 236 | 237 | #[test] 238 | fn test_format_known_address_unspecified() { 239 | let addr = "0.0.0.0".to_string(); 240 | let result = format_known_address(&addr, &AddressType::Unspecified); 241 | assert_eq!(result, "*0.0.0.0*"); 242 | } 243 | 244 | #[test] 245 | fn test_format_known_address_extern() { 246 | let addr = "123.123.123".to_string(); 247 | let result = format_known_address(&addr, &AddressType::Extern); 248 | assert_eq!(result, "123.123.123"); 249 | } 250 | 251 | #[test] 252 | fn test_fill_terminal_width() { 253 | let row = fill_terminal_width(80, [5, 8, 8, 28, 7, 24, 13]); 254 | let columns = row.matches('|').count(); 255 | assert_eq!(columns, 8); // 7 columns + final pipe 256 | } 257 | 258 | #[test] 259 | fn test_table_style_alignment() { 260 | let compact_skin = create_table_style(true); 261 | assert_eq!(compact_skin.table.align, Alignment::Left); 262 | 263 | let non_compact_skin = create_table_style(false); 264 | assert_eq!(non_compact_skin.table.align, Alignment::Center); 265 | } 266 | 267 | #[test] 268 | fn test_get_connections_formatted() { 269 | let connections = vec![ 270 | Connection { 271 | proto: "tcp".to_string(), 272 | local_port: "44796".to_string(), 273 | remote_address: "192.168.1.0".to_string(), 274 | remote_port: "443".to_string(), 275 | program: "firefox".to_string(), 276 | pid: "200".to_string(), 277 | state: "established".to_string(), 278 | address_type: AddressType::Localhost, 279 | ipvx_raw: Ipv4Addr::new(192, 168, 1, 0).into(), 280 | }, 281 | Connection { 282 | proto: "tcp".to_string(), 283 | local_port: "33263".to_string(), 284 | remote_address: "[::ffff:65.9.95.5]".to_string(), 285 | remote_port: "443".to_string(), 286 | program: "-".to_string(), 287 | pid: "-".to_string(), 288 | state: "timewait".to_string(), 289 | address_type: AddressType::Extern, 290 | ipvx_raw: Ipv6Addr::new(0, 0, 0, 0xffff, 65, 9, 95, 5).into(), 291 | }, 292 | ]; 293 | 294 | let template_and_expected_result = [ 295 | ("PID: {{pid}}, Protocol: {{proto}}, Remote Address: {{remote_address}}".to_string(), 296 | "PID: 200, Protocol: tcp, Remote Address: 192.168.1.0\nPID: -, Protocol: tcp, Remote Address: [::ffff:65.9.95.5]".to_string()), 297 | ("Protocol: {{proto}}, Local Port: {{local_port}}, Remote Address: {{remote_address}}, Remote Port: {{remote_port}}, Program: {{program}}, PID: {{pid}}, State: {{state}}, Address Type: {{address_type}}".to_string(), 298 | "Protocol: tcp, Local Port: 44796, Remote Address: 192.168.1.0, Remote Port: 443, Program: firefox, PID: 200, State: established, Address Type: Localhost\nProtocol: tcp, Local Port: 33263, Remote Address: [::ffff:65.9.95.5], Remote Port: 443, Program: -, PID: -, State: timewait, Address Type: Extern".to_string()), 299 | ]; 300 | 301 | for (template, expected_result) in &template_and_expected_result { 302 | let result = get_connections_formatted(&connections, template); 303 | 304 | assert_eq!(result.as_str(), expected_result.as_str()); 305 | } 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::soutln; 2 | 3 | /// Wraps the input text in ANSI escape codes to print it in red. 4 | fn red_text(text: &str) -> String { 5 | format!("\x1b[1;31m{text}\x1b[0m") 6 | } 7 | 8 | /// Wraps the input text in ANSI escape codes to print it in cyan. 9 | fn cyan_text(text: &str) -> String { 10 | format!("\x1B[36m{text}\x1B[0m") 11 | } 12 | 13 | /// Wraps the input text in ANSI escape codes to print it in bold. 14 | fn bold_text(text: &str) -> String { 15 | format!("\x1B[1m{text}\x1B[0m") 16 | } 17 | 18 | /// Prints out formatted text starting with a cyan "Info:" prefix. 19 | /// 20 | /// # Arguments 21 | /// * `text`: The text to print to the console. 22 | /// 23 | /// # Returns 24 | /// None 25 | pub fn pretty_print_info(text: &str) { 26 | soutln!( 27 | "{}", 28 | bold_text(&format!("{} {}", cyan_text("Info:"), bold_text(text))) 29 | ); 30 | } 31 | 32 | /// Prints out formatted text starting with a red "Error:" prefix. 33 | /// 34 | /// # Arguments 35 | /// * `text`: The text to print to the console. 36 | /// 37 | /// # Returns 38 | /// None 39 | pub fn pretty_print_error(text: &str) { 40 | soutln!( 41 | "{}", 42 | bold_text(&format!("{} {}", red_text("Error:"), bold_text(text))) 43 | ); 44 | } 45 | 46 | /// Prints a syntax error message with an error preamble, the error line, and 47 | /// a caret pointing to the error column. 48 | /// 49 | /// # Arguments 50 | /// * `preamble`: The error message or description to print. 51 | /// * `text`: The full source text containing the error. 52 | /// * `line`: The line number (starting at 1) where the error occurred. 53 | /// * `column`: The column number (starting at 1) where the error occurred. 54 | /// 55 | /// # Returns 56 | /// None 57 | /// 58 | /// # Example 59 | /// ``` 60 | /// let code = "let x = 5;\nlet y = 1o;"; 61 | /// pretty_print_syntax_error("Unexpected token", code, 2, 9); 62 | /// ``` 63 | pub fn pretty_print_syntax_error(preamble: &str, text: &str, line: usize, column: usize) { 64 | let erronous_line: &str = text.lines().nth(line - 1).unwrap_or(text); 65 | let line_pointer = "└─>"; 66 | 67 | pretty_print_error(preamble); 68 | soutln!(" {}", red_text("│")); 69 | soutln!(" {} {}", red_text(line_pointer), erronous_line); 70 | soutln!( 71 | " {} {}", 72 | " ".repeat(line_pointer.chars().count() + column - 1), 73 | red_text("^") 74 | ); 75 | } 76 | --------------------------------------------------------------------------------