diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..6fd9e21 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,17 @@ +name: Relatar um problema +description: Algo não funcionou como esperado +labels: [bug] +body: + - type: textarea + id: o-que + attributes: { label: "O que aconteceu?", description: "Passos, o que esperava e o que viu." } + validations: { required: true } + - type: input + id: sistema + attributes: { label: "Sistema operacional e versão do Sabiá", placeholder: "Ubuntu 24.04 / sabia 0.1.0" } + - type: input + id: adaptador + attributes: { label: "Adaptador Wi-Fi e modo", placeholder: "wlan0 (Intel AX200), modo monitor" } + - type: textarea + id: log + attributes: { label: "Saída do terminal (sabia scan / ui)", render: shell } diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..f72a185 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,7 @@ +## O que muda e por quê + +## Checklist +- [ ] `cargo fmt --all` e `cargo clippy --workspace --all-targets` sem avisos +- [ ] `cargo test --workspace` passa +- [ ] Nada neste PR transmite ou envia dados capturados para fora da máquina +- [ ] CHANGELOG atualizado (se houver mudança visível) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e595d1b..bca06a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,12 @@ name: CI on: + # Mudança só de documentação não precisa de 6 jobs em 3 sistemas. push: branches: [main] + paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**'] pull_request: + paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**'] concurrency: group: ci-${{ github.ref }} @@ -23,6 +26,11 @@ jobs: with: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 + - uses: actions/setup-node@v7 + with: + node-version: 22 + - name: Cobertura da tradução da interface + run: node scripts/check-ui-i18n.mjs - run: cargo fmt --all -- --check - run: cargo clippy --workspace --all-targets @@ -60,10 +68,14 @@ jobs: - uses: actions/setup-node@v7 with: node-version: 22 + - name: Cache dos pacotes do Tauri (.deb) + uses: actions/cache@v6 + with: + path: ~/apt-cache + key: apt-tauri-${{ runner.os }}-${{ runner.arch }}-${{ env.ImageVersion }}-${{ hashFiles('scripts/apt-install.sh') }} - name: Dependências do Tauri - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + timeout-minutes: 25 + run: scripts/apt-install.sh ~/apt-cache libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf - name: Gera os ícones run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons - run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7d2266..4fdd05b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -106,11 +106,16 @@ jobs: - uses: actions/setup-node@v7 with: node-version: 22 + - name: Cache dos pacotes do Tauri (.deb) + if: runner.os == 'Linux' + uses: actions/cache@v6 + with: + path: ~/apt-cache + key: apt-tauri-${{ runner.os }}-${{ runner.arch }}-${{ env.ImageVersion }}-${{ hashFiles('scripts/apt-install.sh') }} - name: Dependências do Tauri (Linux) if: runner.os == 'Linux' - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf + timeout-minutes: 25 + run: scripts/apt-install.sh ~/apt-cache libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf - name: Gera os ícones run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons - uses: tauri-apps/tauri-action@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a9b9632..ff9d53a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,23 +1,31 @@ # Changelog +**English** · [Português](CHANGELOG.pt-BR.md) + Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer. ## [0.1.0] - 2026-10-23 -Primeira versão: CLI, app desktop e núcleo compartilhado. +First release: CLI, desktop app and shared core. ### Added -- `sabia-core`: descoberta de interfaces Wi-Fi, controle de modo monitor e - canal (via `iw`/`ip`), captura crua de quadros 802.11 por AF_PACKET (sem - libpcap), parsing de radiotap e de quadros de gerenciamento (beacon, - probe, EAPOL), agregação de APs e clientes vistos, escrita de `.pcap`, - trilha de auditoria local encadeada por hash, auto-atualização por - releases do GitHub. -- `sabia`: CLI com `iface`, `scan`, `update`, `audit-verify` e `ui` - (servidor local da interface gráfica). -- App desktop (Tauri) com a mesma interface servida pelo `sabia ui`, - pedindo elevação de administrador automaticamente ao abrir — modo - monitor e captura crua exigem root/admin. -- Interface gráfica: sidebar de interfaces, painel de captura com linha - de sinal animada, tabela de redes com barras de sinal e badges de - segurança por cor. +- `sabia-core`: Wi-Fi interface discovery, **monitor mode** and channel control + (via netlink/`iw`), **scanning** of 2.4, 5 and 6 GHz, raw 802.11 frame capture + via **AF_PACKET** (no libpcap), **radiotap** and management-frame parsing + (beacon, probe, EAPOL), aggregation of APs and clients, capture of the + **4-way handshake** and **PMKID**, **offline dictionary cracking** + (PBKDF2-HMAC-SHA1, MIC and PMKID) for WPA/WPA2-PSK, `.pcap` read and write, + frame injection, a local hash-chained **audit trail**, and **self-update** + from GitHub releases. +- `sabia`: CLI with `iface` (list/channels/monitor), `scan`, `analyze`, `crack`, + `deauth` (with `--yes`), `doctor`, `audit` (verify/show), `update` and `ui` + (local interface server). +- Desktop app (Tauri) with the same interface served by `sabia ui`, elevating to + administrator automatically on startup — monitor mode and raw capture require + root/admin. +- Interface: interface sidebar, capture panel with an animated signal line, + networks table with signal bars and color-coded security badges, an + **authorized-use** notice, a one-click **update** button, Auto/Light/Dark + themes and **Portuguese and English** language. +- CI with caches (`rust-cache`, cached Tauri `.deb`) and a cross-platform release + pipeline (CLI on 5 targets and desktop app via `tauri-action`). diff --git a/CHANGELOG.pt-BR.md b/CHANGELOG.pt-BR.md new file mode 100644 index 0000000..fbbd373 --- /dev/null +++ b/CHANGELOG.pt-BR.md @@ -0,0 +1,31 @@ +# Changelog + +[English](CHANGELOG.md) · **Português** + +Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer. + +## [0.1.0] - 2026-10-23 + +Primeira versão: CLI, app desktop e núcleo compartilhado. + +### Adicionado +- `sabia-core`: descoberta de interfaces Wi-Fi, controle de **modo monitor** e + canal (via netlink/`iw`), **varredura** de 2.4, 5 e 6 GHz, captura crua de + quadros 802.11 por **AF_PACKET** (sem libpcap), parsing de **radiotap** e de + quadros de gerenciamento (beacon, probe, EAPOL), agregação de APs e clientes, + captura de **4-way handshake** e **PMKID**, **quebra offline** por dicionário + (PBKDF2-HMAC-SHA1, MIC e PMKID) para WPA/WPA2-PSK, escrita e leitura de + **`.pcap`**, injeção de quadros, trilha de **auditoria** local encadeada por + hash, **auto-atualização** por releases do GitHub. +- `sabia`: CLI com `iface` (list/channels/monitor), `scan`, `analyze`, `crack`, + `deauth` (com `--yes`), `doctor`, `audit` (verify/show), `update` e `ui` + (servidor local da interface). +- App desktop (Tauri) com a mesma interface servida pelo `sabia ui`, pedindo + elevação de administrador automaticamente ao abrir — modo monitor e captura + crua exigem root/admin. +- Interface gráfica: sidebar de interfaces, painel de captura com linha de sinal + animada, tabela de redes com barras de sinal e selos de segurança por cor, + aviso de **uso autorizado**, botão de **atualização** em um clique, temas Auto/ + Claro/Escuro e idioma **português e inglês**. +- CI com caches (`rust-cache`, `.deb` do Tauri em cache) e pipeline de release + multiplataforma (CLI em 5 alvos e app desktop via `tauri-action`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d3d40ef --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to Sabiá + +**English** · [Português](CONTRIBUTING.pt-BR.md) + +Thanks for helping! Golden rules: + +1. **Authorized auditing only.** Every new feature is for testing your own networks or networks you're authorized to test. Sensitive operations (deauthentication) require explicit confirmation and are recorded in the audit trail. PRs that hide activity from the trail or remove the confirmations are rejected. +2. **Test the parsers with real data.** New readers (radiotap, 802.11, EAPOL, pcap) need tests against known frames — see `crates/sabia-core/tests/` and the helpers in `crates/sabia-core/src/testutil.rs`. No parser without a test. +3. **Be honest about what works.** If a feature is Linux-only, or only covers WPA2-PSK, say so in `--help`, in the README and in the test. + +## Workflow + +```bash +cargo fmt --all +cargo clippy --workspace --all-targets # as in CI (RUSTFLAGS=-D warnings) +cargo test --workspace +node scripts/check-ui-i18n.mjs # UI translation coverage +``` + +CI runs on Linux, Windows and macOS; run these before pushing — almost everything that fails CI shows up here in seconds. Documentation-only changes (`*.md`, `docs/`) are light on CI. + +Live capture exists only on Linux (`AF_PACKET` + netlink); on Windows and macOS those paths compile but aren't exercised. Parsing and cracking tests run on all three, because they're offline. + +## Where to work + +Module map in [docs/architecture.md](docs/architecture.md). What's missing and welcome is in the [ROADMAP](ROADMAP.md). + +- **Frame parsing:** `radiotap.rs`, `dot11.rs`, `eapol.rs`. +- **Radio and system:** `iface.rs`, `netlink.rs`, `scan.rs`, `capture.rs`, `inject.rs`. +- **Handshake and cracking:** `handshake.rs`, `crack.rs`. +- **App and API:** `crates/sabia-cli/src/` and `apps/gui/`. + +## Interface and translation + +The source text of the interface is **Portuguese**; English comes from `apps/gui/ui/i18n-en.js`, applied at runtime by `i18n.js`. When adding or changing visible text, run `node scripts/check-ui-i18n.mjs` and include the translation — CI rejects Portuguese without an English pair. Don't translate user data (SSID, BSSID, values) or technical terms (WPA, EAPOL, pcap…). + +## Documentation + +Keep the pt/en pair: every `*.md` and `docs/*` file has a Portuguese version (`*.pt-BR.md`, `docs/pt-BR/`) and an English one. + +## License + +By contributing, you agree to license under [MIT](LICENSE-MIT) **or** [Apache-2.0](LICENSE-APACHE), like the rest of the project. diff --git a/CONTRIBUTING.pt-BR.md b/CONTRIBUTING.pt-BR.md new file mode 100644 index 0000000..87c6243 --- /dev/null +++ b/CONTRIBUTING.pt-BR.md @@ -0,0 +1,43 @@ +# Contribuindo com o Sabiá + +[English](CONTRIBUTING.md) · **Português** + +Obrigado por ajudar! Regras de ouro: + +1. **Só auditoria autorizada.** Todo recurso novo é para testar redes próprias ou com autorização. Operações sensíveis (desautenticação) exigem confirmação explícita e ficam na trilha de auditoria. PRs que escondem atividade da trilha ou removem as confirmações são recusados. +2. **Teste os parsers com dados reais.** Novos leitores (radiotap, 802.11, EAPOL, pcap) precisam de teste com quadros conhecidos — veja `crates/sabia-core/tests/` e os ajudantes em `crates/sabia-core/src/testutil.rs`. Nada de parser sem teste. +3. **Seja honesto sobre o que funciona.** Se um recurso só roda no Linux, ou só cobre WPA2-PSK, diga isso no `--help`, no README e no teste. + +## Fluxo + +```bash +cargo fmt --all +cargo clippy --workspace --all-targets # como no CI (RUSTFLAGS=-D warnings) +cargo test --workspace +node scripts/check-ui-i18n.mjs # cobertura da tradução da interface +``` + +O CI roda em Linux, Windows e macOS; rode esses comandos antes do push — quase tudo que faz o CI falhar aparece aqui em segundos. Mudanças só de documentação (`*.md`, `docs/`) são leves no CI. + +A captura ao vivo só existe no Linux (`AF_PACKET` + netlink); em Windows e macOS esses caminhos são compilados mas não exercitados. Testes de parsing e de quebra rodam nas três plataformas, porque são offline. + +## Onde mexer + +Mapa dos módulos em [docs/pt-BR/arquitetura.md](docs/pt-BR/arquitetura.md). O que falta e o que é bem-vindo está no [ROADMAP](ROADMAP.pt-BR.md). + +- **Parsing de quadros:** `radiotap.rs`, `dot11.rs`, `eapol.rs`. +- **Rádio e sistema:** `iface.rs`, `netlink.rs`, `scan.rs`, `capture.rs`, `inject.rs`. +- **Handshake e quebra:** `handshake.rs`, `crack.rs`. +- **App e API:** `crates/sabia-cli/src/` e `apps/gui/`. + +## Interface e tradução + +O texto-fonte da interface é em **português**; o inglês vem de `apps/gui/ui/i18n-en.js`, aplicado em tempo de execução por `i18n.js`. Ao adicionar ou mudar texto visível, rode `node scripts/check-ui-i18n.mjs` e inclua a tradução — o CI recusa português sem par em inglês. Não traduza dados do usuário (SSID, BSSID, valores) nem termos técnicos (WPA, EAPOL, pcap…). + +## Documentação + +Mantenha o par pt/en: todo arquivo `*.md` e `docs/*` tem a versão em português (`*.pt-BR.md`, `docs/pt-BR/`) e em inglês. + +## Licença + +Ao contribuir, você concorda em licenciar sob [MIT](LICENSE-MIT) **ou** [Apache-2.0](LICENSE-APACHE), como o resto do projeto. diff --git a/Cargo.lock b/Cargo.lock index cddb092..616e74f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -192,6 +193,22 @@ dependencies = [ "objc2", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "generic-array" version = "0.14.7" @@ -202,12 +219,32 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -226,6 +263,22 @@ version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.3" @@ -265,6 +318,12 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -295,6 +354,25 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "sabia-cli" version = "0.1.0" @@ -302,6 +380,7 @@ dependencies = [ "clap", "ctrlc", "sabia-core", + "serde", "serde_json", ] @@ -309,10 +388,14 @@ dependencies = [ name = "sabia-core" version = "0.1.0" dependencies = [ + "hmac", "libc", + "md-5", "serde", "serde_json", + "sha1", "sha2", + "tempfile", "thiserror", "time", ] @@ -360,6 +443,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -377,6 +471,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -399,6 +499,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/README.md b/README.md index ab327db..59a1f2b 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,112 @@ -# Sabiá +

Sabiá

+

Sabiá

+

Free and open Wi-Fi auditing.
Terminal and desktop app · live capture on Linux, offline analysis on Windows, Linux and macOS · authorized use.

-Auditoria, monitoramento e pentest de redes Wi-Fi — terminal e interface -gráfica sobre o mesmo núcleo, com foco na interface gráfica e uso -intuitivo: a parte complexa (netlink, radiotap, parsing de quadro 802.11) -fica escondida em `sabia-core`; CLI e GUI só consomem. +

Terminal · Interface · Architecture · Roadmap · Changelog

-> Use apenas em redes próprias ou com autorização explícita do responsável -> pela rede (engajamento de pentest formal). Toda ativação de modo monitor -> e sessão de captura é registrada numa trilha de auditoria local -> encadeada por hash (`.sabia-audit.log`). +

English · Português

-## Estrutura +--- -``` -crates/sabia-core biblioteca: netlink (iw), radiotap, parsing 802.11, - captura crua (AF_PACKET), auditoria, auto-atualização -crates/sabia-cli binário `sabia`: comandos de terminal + servidor - local da interface (`sabia ui`) -apps/gui/ui mesma página HTML/JS servida pelo `sabia ui` e pelo app desktop -apps/gui/src-tauri app desktop (Tauri) — pede administrador/root ao abrir +Sabiá brings together, in a single sweep, what tools like `airodump-ng`, `hcxdumptool`, `aircrack-ng` and `wifite` do separately — in one Rust binary, with no `libpcap` dependency: + +- **Network discovery:** hops across the **2.4, 5 and 6 GHz** channels and lists access points and associated clients, with SSID, BSSID, channel, signal and security type. +- **Monitor mode without scripts:** turns the card's monitor mode on and off straight through netlink/`iw`, and shows which channels the radio supports. +- **Handshake capture:** recognizes the **4-way handshake (EAPOL)** and the **PMKID** on the air and keeps only what offline cracking needs. +- **Offline dictionary cracking:** recomputes the PMK with **PBKDF2-HMAC-SHA1** and checks the **MIC** (4-way) or the **PMKID** (`HMAC-SHA1(PMK, "PMK Name" …)`) — for WPA/WPA2-PSK. +- **pcap round-trip:** writes captured frames to `.pcap` and **analyzes a `.pcap` offline** (networks, clients and handshakes), interoperable with the other tools. +- **Deauthentication test:** sends _deauth_ to an AP/station to force a reassociation — only with an explicit authorization confirmation (`--yes`). +- **Audit trail:** every monitor-mode activation and capture session is recorded in a local **hash-chained** trail. +- **Diagnostics:** `doctor` checks privileges, tools and interfaces before you start. + +## Authorized use + +Sabiá is an **auditing** tool — for testing the security of networks you **own** or have **explicit authorization** to assess. + +- Capturing handshakes, sending deauthentication or cracking passwords on third-party networks **without permission is illegal** in most countries. The responsibility is the operator's. +- Deauthentication (`deauth`) requires the `--yes` flag, an assertion that you are authorized to test that network. +- Every monitor-mode activation and capture session is **recorded in the local audit trail** (`sabia audit show`), hash-chained to detect tampering. +- **Nothing of yours leaves the machine.** The only network access by default is the update check on startup (which can be disabled). +- Dictionary passwords and handshakes stay on your machine. + +## Install + +Download the package for your system from **[Releases](../../releases)**: + +| | CLI | Desktop app | +|---|---|---| +| Windows | `sabia-*-windows-x86_64.zip` | `.msi` / `.exe` installer | +| macOS | `sabia-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` | +| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` | + +Each file has a `.sha256` next to it. Build from source: `cargo build --release -p sabia-cli` (Rust 1.80+). + +**Updates:** the desktop app checks GitHub on startup; when a new version exists, a button offers **Update now**: it downloads the right binary for your platform, verifies the **SHA-256** and swaps the running executable — no manual download. The check is a single request to the releases API and sends nothing of yours. + +> The binaries are **not signed yet**: Windows SmartScreen and macOS Gatekeeper will warn. Check the SHA-256. + +**Capture and monitor mode require Linux and root** (`CAP_NET_ADMIN`/`CAP_NET_RAW`): raw capture uses `AF_PACKET` and interface control uses netlink. **Offline `.pcap` analysis and cracking** also run on Windows and macOS. The desktop app elevates to administrator on its own at startup. + +## Getting started + +```bash +sabia doctor # privileges, tools and interfaces +sabia iface list # Wi-Fi interfaces and their modes +sudo sabia iface monitor wlan0 on # enable monitor mode + +sudo sabia scan wlan0 --band all --out air.pcap # hop channels and record the air +sabia analyze air.pcap # read a .pcap offline +sabia crack air.pcap --wordlist list.txt # crack the handshake by dictionary ``` -## Uso +Authorized deauthentication testing and all other commands (`audit`, `update`, `ui`) are in **[docs/cli.md](docs/cli.md)**. + +## Desktop app + +Desktop app (Tauri) with an interface sidebar, a capture panel with an animated signal line, and a networks table with signal bars and color-coded security badges. The same front-end runs in the browser with `sabia ui`. More in **[docs/interface.md](docs/interface.md)**. + +- Pick the interface, toggle **monitor mode** and start/stop **capture** by button. +- Live **networks seen** table, with signal, channel and security. +- An **authorized-use** notice always in view and automatic privilege elevation on startup. +- One-click **update** button, **Auto, Light and Dark** themes and interface in **Portuguese and English**. + +## Known limits + +- **Capture is Linux-only**, with a card that supports **monitor mode** (and injection, for `deauth`). On Windows and macOS Sabiá only does `.pcap` analysis and offline cracking. +- **WPA3/SAE** does not fall to handshake dictionaries — cracking covers **WPA/WPA2-PSK** (4-way and PMKID). +- **5 and 6 GHz** depend on the card and the regulatory domain; some channels stay passive (no probing). +- **Passwords outside the dictionary don't come back:** cracking is dictionary force only, no rules or masks yet. +- **Windows and macOS:** the code compiles and is tested in CI, but live capture does not run on those platforms. + +## Where it's going + +Summary of the [ROADMAP](ROADMAP.md): + +| | | +|---|---| +| **Done** | Monitor mode via netlink, 2.4/5/6 GHz scanning, 4-way and PMKID capture, `.pcap` read/write, dictionary cracking (WPA/WPA2-PSK), authorized `deauth`, hash-chained audit trail, `doctor`, CLI + desktop app, one-click update | +| **Next** | Rules and masks in cracking, GPU acceleration, export to the hashcat format (`22000`) | +| **Later** | WPS attack, rogue AP / evil twin for authorized testing, capture on Windows (Npcap) | +| **Long term** | WPA3/SAE downgrade, validation on varied hardware | + +## Development ``` -sabia iface list # lista interfaces, marca as Wi-Fi -sabia iface monitor on # liga modo monitor (precisa root) -sabia scan --out captura.pcap # varre canais, mostra APs/clientes -sabia ui # abre a interface gráfica local -sabia update # verifica atualização -sabia update --apply # baixa e aplica a atualização +crates/sabia-core core: interfaces, netlink, radiotap, 802.11, EAPOL, handshake, capture, crack, audit, update +crates/sabia-cli the `sabia` binary (CLI + local UI server) +apps/gui interface (ui/ = HTML, CSS and JS; src-tauri/ = desktop app) +scripts/ CI helpers (cached dependency install, i18n coverage check) +docs/ terminal, interface and architecture ``` -Ou baixe o app desktop em [Releases](https://github.com/lucasrguerra/sabia/releases) -— ele pede elevação de administrador/root automaticamente ao abrir, já que -modo monitor e captura crua exigem esse privilégio em qualquer sistema. +```bash +cargo test --workspace +cargo clippy --workspace --all-targets +node scripts/check-ui-i18n.mjs # UI translation coverage +``` -Pela CLI, capturar quadros 802.11 exige `CAP_NET_RAW`/`CAP_NET_ADMIN` — -normalmente root (`sudo sabia ...`). +See [CONTRIBUTING.md](CONTRIBUTING.md) and [docs/architecture.md](docs/architecture.md). Versions follow [SemVer](https://semver.org/); changes in [CHANGELOG.md](CHANGELOG.md). To release: `git tag vX.Y.Z && git push --tags` (the `release` workflow builds and publishes everything). Vulnerabilities: [SECURITY.md](SECURITY.md). -## Dependências externas +## License -`iw` e `ip` (iproute2) para controlar modo monitor e canal — é uma ponte -deliberada até existir uma implementação nl80211 nativa. A captura em si -(AF_PACKET) e todo o parsing de quadro são Rust puro, sem libpcap. - -## Hardware testado - -Adaptador USB RTL8812AU (driver `rtw88_8812au`), suporta modo monitor e -injeção. +[MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), your choice. diff --git a/README.pt-BR.md b/README.pt-BR.md new file mode 100644 index 0000000..8802288 --- /dev/null +++ b/README.pt-BR.md @@ -0,0 +1,112 @@ +

Sabiá

+

Sabiá

+

Auditoria de Wi-Fi livre e gratuita.
Terminal e interface gráfica · captura no Linux, análise offline em Windows, Linux e macOS · uso autorizado.

+ +

Terminal · Interface · Arquitetura · Roadmap · Changelog

+ +

English · Português

+ +--- + +O Sabiá junta, numa única varredura, o que ferramentas como `airodump-ng`, `hcxdumptool`, `aircrack-ng` e `wifite` fazem separadamente — em um só binário Rust, sem depender de `libpcap`: + +- **Descoberta de redes:** percorre os canais de **2.4, 5 e 6 GHz** e lista os pontos de acesso e os clientes associados, com SSID, BSSID, canal, sinal e tipo de segurança. +- **Modo monitor sem scripts:** liga e desliga o modo monitor da placa direto pelo netlink/`iw`, e mostra os canais que o rádio suporta. +- **Captura de handshake:** reconhece o **4-way handshake (EAPOL)** e o **PMKID** no ar e guarda só o que é preciso para a quebra offline. +- **Quebra offline por dicionário:** recalcula a PMK com **PBKDF2-HMAC-SHA1** e confere o **MIC** (4-way) ou o **PMKID** (`HMAC-SHA1(PMK, "PMK Name" …)`) — para WPA/WPA2-PSK. +- **pcap de ida e volta:** grava os quadros capturados em `.pcap` e **analisa um `.pcap` offline** (redes, clientes e handshakes), compatível com as outras ferramentas. +- **Teste de desautenticação:** envia _deauth_ a um AP/estação para forçar uma reassociação — só com a confirmação explícita de autorização (`--yes`). +- **Trilha de auditoria:** toda ativação de modo monitor e sessão de captura fica registrada numa trilha local **encadeada por hash**. +- **Diagnóstico:** o `doctor` confere privilégios, ferramentas e interfaces antes de você começar. + +## Uso autorizado + +O Sabiá é uma ferramenta de **auditoria** — para testar a segurança de redes **suas** ou de redes que você tem **autorização explícita** para avaliar. + +- Capturar handshakes, enviar desautenticação ou quebrar senhas de redes de terceiros **sem permissão é ilegal** na maioria dos países. A responsabilidade é de quem usa. +- A desautenticação (`deauth`) exige a flag `--yes`, uma afirmação de que você tem autorização para testar aquela rede. +- Toda ativação de modo monitor e sessão de captura é **registrada na trilha de auditoria local** (`sabia audit show`), com encadeamento por hash para detectar adulteração. +- **Nada seu é enviado para fora.** O único acesso à rede por padrão é a verificação de atualização ao abrir o app (desligável). +- Senhas de dicionário e handshakes ficam só na sua máquina. + +## Instalação + +Baixe o pacote do seu sistema em **[Releases](../../releases)**: + +| | CLI | App gráfico | +|---|---|---| +| Windows | `sabia-*-windows-x86_64.zip` | instalador `.msi` / `.exe` | +| macOS | `sabia-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` | +| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` | + +Cada arquivo tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p sabia-cli` (Rust 1.80+). + +**Atualizações:** o app desktop consulta o GitHub ao abrir; havendo versão nova, um botão oferece **Atualizar agora**: ele baixa o binário certo para a plataforma, confere o **SHA-256** e troca o executável em uso — você não precisa baixar nada na mão. A consulta é uma única requisição à API de releases e nada seu é enviado. + +> Os binários ainda **não são assinados**: o SmartScreen (Windows) e o Gatekeeper (macOS) vão avisar. Confira o SHA-256. + +**Captura e modo monitor exigem Linux e privilégio de root** (`CAP_NET_ADMIN`/`CAP_NET_RAW`): a captura crua usa `AF_PACKET` e o controle de interface usa netlink. A **análise de `.pcap` e a quebra offline** rodam também em Windows e macOS. O app desktop pede elevação de administrador sozinho ao abrir. + +## Começando + +```bash +sabia doctor # privilégios, ferramentas e interfaces +sabia iface list # interfaces Wi-Fi e seus modos +sudo sabia iface monitor wlan0 on # liga o modo monitor + +sudo sabia scan wlan0 --band all --out ar.pcap # percorre os canais e grava o ar +sabia analyze ar.pcap # lê um .pcap offline +sabia crack ar.pcap --wordlist lista.txt # quebra o handshake por dicionário +``` + +Teste autorizado de desautenticação e todos os outros comandos (`audit`, `update`, `ui`) estão em **[docs/pt-BR/cli.md](docs/pt-BR/cli.md)**. + +## Interface gráfica + +App desktop (Tauri) com sidebar de interfaces, painel de captura com linha de sinal animada e uma tabela de redes com barras de sinal e selos de segurança por cor. O mesmo front-end roda no navegador com `sabia ui`. Mais detalhes em **[docs/pt-BR/interface.md](docs/pt-BR/interface.md)**. + +- Seleção da interface, ativação de **modo monitor** e início/parada de **captura** por botão. +- Tabela de **redes vistas** em tempo real, com sinal, canal e segurança. +- Aviso de **uso autorizado** sempre à vista e elevação automática de privilégio ao abrir. +- Botão de **atualização** em um clique, temas **Auto, Claro e Escuro** e interface em **português e inglês**. + +## Limites conhecidos + +- **Captura só no Linux**, com placa que suporte **modo monitor** (e injeção, para `deauth`). Em Windows e macOS o Sabiá faz só análise de `.pcap` e quebra offline. +- **WPA3/SAE** não cai por dicionário de handshake — a quebra cobre **WPA/WPA2-PSK** (4-way e PMKID). +- **5 e 6 GHz** dependem da placa e do _regulatory domain_; alguns canais ficam passivos (sem _probe_). +- **Senhas fora do dicionário não voltam:** a quebra é só força de dicionário, sem regras nem máscara ainda. +- **Windows e macOS:** o código compila e é testado em CI, mas a captura ao vivo não roda nessas plataformas. + +## Para onde vai + +Resumo do [ROADMAP](ROADMAP.pt-BR.md): + +| | | +|---|---| +| **Feito** | Modo monitor por netlink, varredura 2.4/5/6 GHz, captura de 4-way e PMKID, escrita e leitura de `.pcap`, quebra por dicionário (WPA/WPA2-PSK), `deauth` autorizado, trilha de auditoria por hash, `doctor`, CLI + app desktop, atualização em um clique | +| **Próximo** | Regras e máscaras na quebra, aceleração por GPU, exportação para o formato do hashcat (`22000`) | +| **Depois** | Ataque a WPS, _rogue AP_/_evil twin_ para teste autorizado, captura em Windows (Npcap) | +| **Longo prazo** | WPA3/SAE _downgrade_, validação em hardware variado | + +## Desenvolvimento + +``` +crates/sabia-core núcleo: interfaces, netlink, radiotap, 802.11, EAPOL, handshake, captura, crack, auditoria, update +crates/sabia-cli binário `sabia` (CLI + servidor local da UI) +apps/gui interface (ui/ = HTML, CSS e JS; src-tauri/ = app desktop) +scripts/ apoio de CI (instalação de dependências em cache, checagem de i18n) +docs/ terminal, interface e arquitetura +``` + +```bash +cargo test --workspace +cargo clippy --workspace --all-targets +node scripts/check-ui-i18n.mjs # cobertura da tradução da interface +``` + +Veja [CONTRIBUTING.pt-BR.md](CONTRIBUTING.pt-BR.md) e [docs/pt-BR/arquitetura.md](docs/pt-BR/arquitetura.md). Versões seguem [SemVer](https://semver.org/lang/pt-BR/); mudanças em [CHANGELOG.pt-BR.md](CHANGELOG.pt-BR.md). Para publicar: `git tag vX.Y.Z && git push --tags` (o workflow `release` compila e publica tudo). Vulnerabilidades: [SECURITY.pt-BR.md](SECURITY.pt-BR.md). + +## Licença + +[MIT](LICENSE-MIT) ou [Apache-2.0](LICENSE-APACHE), à sua escolha. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..2cfd777 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,35 @@ +# Roadmap + +**English** · [Português](ROADMAP.pt-BR.md) + +Sabiá does Wi-Fi auditing: it discovers networks, captures handshakes (4-way and PMKID), cracks WPA/WPA2-PSK by offline dictionary and tests deauthentication with authorization. Always for authorized use, with a local audit trail. + +## Done (0.1.0) + +- **Monitor mode** and channel control via netlink/`iw`. +- **Scanning** of 2.4, 5 and 6 GHz: APs and clients, with SSID, BSSID, channel, signal and security. +- Raw 802.11 capture via **AF_PACKET** (no libpcap), **radiotap** and management-frame parsing. +- Capture of the **4-way handshake (EAPOL)** and the **PMKID**. +- **Offline dictionary cracking** (PBKDF2-HMAC-SHA1, MIC and PMKID) for WPA/WPA2-PSK. +- **`.pcap`** read and write; offline `.pcap` analysis. +- Authorized **`deauth`** (`--yes` flag). +- Local hash-chained **audit trail**; `doctor` diagnostics. +- **CLI** + **desktop app** (Tauri) + local UI server; **one-click update**. + +## Next + +- **Rules and masks** in cracking (hashcat style), beyond the plain dictionary. +- **`22000` export** to take the handshake straight to hashcat/John. +- **GPU** acceleration for PMK derivation. + +## Later + +- **WPS attack** (PIN/Pixie Dust) in authorized testing. +- **Rogue AP / evil twin** for client assessment, with authorization. +- **Capture on Windows** via Npcap. + +## Long term + +- **WPA3/SAE**: detection and downgrade paths where applicable. +- Validation on **varied hardware** (chipsets and regulatory domains). +- Signing of release binaries (SmartScreen/Gatekeeper). diff --git a/ROADMAP.pt-BR.md b/ROADMAP.pt-BR.md new file mode 100644 index 0000000..d83f28e --- /dev/null +++ b/ROADMAP.pt-BR.md @@ -0,0 +1,35 @@ +# Roadmap + +[English](ROADMAP.md) · **Português** + +O Sabiá faz auditoria de Wi-Fi: descobre redes, captura handshakes (4-way e PMKID), quebra WPA/WPA2-PSK por dicionário offline e testa desautenticação com autorização. Sempre de uso autorizado, com trilha de auditoria local. + +## Feito (0.1.0) + +- Controle de **modo monitor** e de canal por netlink/`iw`. +- **Varredura** de 2.4, 5 e 6 GHz: APs e clientes, com SSID, BSSID, canal, sinal e segurança. +- Captura crua de 802.11 por **AF_PACKET** (sem libpcap), parsing de **radiotap** e de quadros de gerenciamento. +- Captura de **4-way handshake (EAPOL)** e de **PMKID**. +- **Quebra offline** por dicionário (PBKDF2-HMAC-SHA1, MIC e PMKID) para WPA/WPA2-PSK. +- Escrita e leitura de **`.pcap`**; análise de `.pcap` offline. +- **`deauth`** autorizado (flag `--yes`). +- **Trilha de auditoria** local encadeada por hash; `doctor` de diagnóstico. +- **CLI** + **app desktop** (Tauri) + servidor local da UI; **atualização em um clique**. + +## Próximo + +- **Regras e máscaras** na quebra (estilo hashcat), além do dicionário puro. +- **Exportação `22000`** para levar o handshake direto ao hashcat/John. +- Aceleração por **GPU** na derivação da PMK. + +## Depois + +- **Ataque a WPS** (PIN/Pixie Dust) em teste autorizado. +- **Rogue AP / evil twin** para avaliação de clientes, com autorização. +- **Captura em Windows** via Npcap. + +## Longo prazo + +- **WPA3/SAE**: detecção e caminhos de _downgrade_ onde aplicável. +- Validação em **hardware variado** (chipsets e _regulatory domains_). +- Assinatura dos binários de release (SmartScreen/Gatekeeper). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..6d6a072 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security + +**English** · [Português](SECURITY.pt-BR.md) + +Sabiá processes untrusted data from the air and from `.pcap` files (802.11 frames and handshakes, possibly forged) and controls network interfaces with root privilege. Vulnerabilities — memory corruption in the radiotap/802.11/EAPOL parsers, path escape when writing `.pcap` or exporting, privilege misuse — should be reported privately through the repository's **Security → Report a vulnerability** tab on GitHub. Do not open a public issue before a fix. + +Sabiá is a tool for **authorized auditing**. Reports about using it to attack third-party networks are out of the project's security scope — see the **Authorized use** section of the [README](README.md). diff --git a/SECURITY.pt-BR.md b/SECURITY.pt-BR.md new file mode 100644 index 0000000..75314e1 --- /dev/null +++ b/SECURITY.pt-BR.md @@ -0,0 +1,7 @@ +# Segurança + +[English](SECURITY.md) · **Português** + +O Sabiá processa dados não confiáveis vindos do ar e de arquivos `.pcap` (quadros 802.11 e handshakes, possivelmente forjados) e controla interfaces de rede com privilégio de root. Vulnerabilidades — corrupção de memória nos parsers de radiotap/802.11/EAPOL, escape de caminho na escrita de `.pcap` ou na exportação, uso indevido de privilégio — devem ser reportadas em privado pela aba **Security → Report a vulnerability** do repositório no GitHub. Não abra issue pública antes da correção. + +O Sabiá é uma ferramenta de **auditoria autorizada**. Relatos sobre uso para atacar redes de terceiros não são escopo de segurança do projeto — veja a seção **Uso autorizado** do [README](README.pt-BR.md). diff --git a/apps/gui/src-tauri/Cargo.lock b/apps/gui/src-tauri/Cargo.lock new file mode 100644 index 0000000..c455f1c --- /dev/null +++ b/apps/gui/src-tauri/Cargo.lock @@ -0,0 +1,4348 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.9", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.9", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "defmt-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.8+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.17+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "sabia-core" +version = "0.1.0" +dependencies = [ + "hmac", + "libc", + "md-5", + "serde", + "serde_json", + "sha1", + "sha2", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "sabia-gui" +version = "0.1.0" +dependencies = [ + "libc", + "sabia-core", + "serde_json", + "tauri", + "tauri-build", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.3", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_vcruntime" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "static_vcruntime", + "strsim", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316" +dependencies = [ + "base64 0.23.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.21", + "toml 1.1.8+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52bb6904475a6ca662f15ef787f18ade61d5740a1ee12c45d3b748efdb6f796" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.8+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.129" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.21", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" +dependencies = [ + "base64 0.23.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/gui/src-tauri/gen/schemas/acl-manifests.json b/apps/gui/src-tauri/gen/schemas/acl-manifests.json new file mode 100644 index 0000000..2ebbc8d --- /dev/null +++ b/apps/gui/src-tauri/gen/schemas/acl-manifests.json @@ -0,0 +1 @@ +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-exit":{"identifier":"allow-exit","description":"Enables the exit command without any pre-configured scope.","commands":{"allow":["exit"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-exit":{"identifier":"deny-exit","description":"Denies the exit command without any pre-configured scope.","commands":{"allow":[],"deny":["exit"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-fullscreen-on-monitor":{"identifier":"allow-set-fullscreen-on-monitor","description":"Enables the set_fullscreen_on_monitor command without any pre-configured scope.","commands":{"allow":["set_fullscreen_on_monitor"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-fullscreen-on-monitor":{"identifier":"deny-set-fullscreen-on-monitor","description":"Denies the set_fullscreen_on_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen_on_monitor"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/apps/gui/src-tauri/gen/schemas/capabilities.json b/apps/gui/src-tauri/gen/schemas/capabilities.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/apps/gui/src-tauri/gen/schemas/capabilities.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/apps/gui/src-tauri/gen/schemas/desktop-schema.json b/apps/gui/src-tauri/gen/schemas/desktop-schema.json new file mode 100644 index 0000000..1ef1225 --- /dev/null +++ b/apps/gui/src-tauri/gen/schemas/desktop-schema.json @@ -0,0 +1,2316 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-exit", + "markdownDescription": "Enables the exit command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-exit", + "markdownDescription": "Denies the exit command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen-on-monitor", + "markdownDescription": "Enables the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen-on-monitor", + "markdownDescription": "Denies the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/apps/gui/src-tauri/gen/schemas/linux-schema.json b/apps/gui/src-tauri/gen/schemas/linux-schema.json new file mode 100644 index 0000000..1ef1225 --- /dev/null +++ b/apps/gui/src-tauri/gen/schemas/linux-schema.json @@ -0,0 +1,2316 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CapabilityFile", + "description": "Capability formats accepted in a capability file.", + "anyOf": [ + { + "description": "A single capability.", + "allOf": [ + { + "$ref": "#/definitions/Capability" + } + ] + }, + { + "description": "A list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + }, + { + "description": "A list of capabilities.", + "type": "object", + "required": [ + "capabilities" + ], + "properties": { + "capabilities": { + "description": "The list of capabilities.", + "type": "array", + "items": { + "$ref": "#/definitions/Capability" + } + } + } + } + ], + "definitions": { + "Capability": { + "description": "A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\n\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\n\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\n\n## Example\n\n```json { \"identifier\": \"main-user-files-write\", \"description\": \"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\", \"windows\": [ \"main\" ], \"permissions\": [ \"core:default\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] }, ], \"platforms\": [\"macOS\",\"windows\"] } ```", + "type": "object", + "required": [ + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "Identifier of the capability.\n\n## Example\n\n`main-user-files-write`", + "type": "string" + }, + "description": { + "description": "Description of what the capability is intended to allow on associated windows.\n\nIt should contain a description of what the grouped permissions should allow.\n\n## Example\n\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.", + "default": "", + "type": "string" + }, + "remote": { + "description": "Configure remote URLs that can use the capability permissions.\n\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\n\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\n\n## Example\n\n```json { \"urls\": [\"https://*.mydomain.dev\"] } ```", + "anyOf": [ + { + "$ref": "#/definitions/CapabilityRemote" + }, + { + "type": "null" + } + ] + }, + "local": { + "description": "Whether this capability is enabled for local app URLs or not. Defaults to `true`.", + "default": true, + "type": "boolean" + }, + "windows": { + "description": "List of windows that are affected by this capability. Can be a glob pattern.\n\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\n\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\n\n## Example\n\n`[\"main\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "webviews": { + "description": "List of webviews that are affected by this capability. Can be a glob pattern.\n\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\n\n## Example\n\n`[\"sub-webview-one\", \"sub-webview-two\"]`", + "type": "array", + "items": { + "type": "string" + } + }, + "permissions": { + "description": "List of permissions attached to this capability.\n\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\n\n## Example\n\n```json [ \"core:default\", \"shell:allow-open\", \"dialog:open\", { \"identifier\": \"fs:allow-write-text-file\", \"allow\": [{ \"path\": \"$HOME/test.txt\" }] } ] ```", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionEntry" + }, + "uniqueItems": true + }, + "platforms": { + "description": "Limit which target platforms this capability applies to.\n\nBy default all platforms are targeted.\n\n## Example\n\n`[\"macOS\",\"windows\"]`", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "CapabilityRemote": { + "description": "Configuration for remote URLs that are associated with the capability.", + "type": "object", + "required": [ + "urls" + ], + "properties": { + "urls": { + "description": "Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\n\n## Examples\n\n- \"https://*.mydomain.dev\": allows subdomains of mydomain.dev - \"https://mydomain.dev/api/*\": allows any subpath of mydomain.dev/api", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionEntry": { + "description": "An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.", + "anyOf": [ + { + "description": "Reference a permission or permission set by identifier.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + { + "description": "Reference a permission or permission set by identifier and extends its scope.", + "type": "object", + "allOf": [ + { + "properties": { + "identifier": { + "description": "Identifier of the permission or permission set.", + "allOf": [ + { + "$ref": "#/definitions/Identifier" + } + ] + }, + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + } + ], + "required": [ + "identifier" + ] + } + ] + }, + "Identifier": { + "description": "Permission identifier", + "oneOf": [ + { + "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", + "type": "string", + "const": "core:default", + "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", + "type": "string", + "const": "core:app:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" + }, + { + "description": "Enables the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-hide", + "markdownDescription": "Enables the app_hide command without any pre-configured scope." + }, + { + "description": "Enables the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-app-show", + "markdownDescription": "Enables the app_show command without any pre-configured scope." + }, + { + "description": "Enables the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-bundle-type", + "markdownDescription": "Enables the bundle_type command without any pre-configured scope." + }, + { + "description": "Enables the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-default-window-icon", + "markdownDescription": "Enables the default_window_icon command without any pre-configured scope." + }, + { + "description": "Enables the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-exit", + "markdownDescription": "Enables the exit command without any pre-configured scope." + }, + { + "description": "Enables the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-fetch-data-store-identifiers", + "markdownDescription": "Enables the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Enables the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-identifier", + "markdownDescription": "Enables the identifier command without any pre-configured scope." + }, + { + "description": "Enables the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-name", + "markdownDescription": "Enables the name command without any pre-configured scope." + }, + { + "description": "Enables the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-register-listener", + "markdownDescription": "Enables the register_listener command without any pre-configured scope." + }, + { + "description": "Enables the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-data-store", + "markdownDescription": "Enables the remove_data_store command without any pre-configured scope." + }, + { + "description": "Enables the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-remove-listener", + "markdownDescription": "Enables the remove_listener command without any pre-configured scope." + }, + { + "description": "Enables the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-app-theme", + "markdownDescription": "Enables the set_app_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-set-dock-visibility", + "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Enables the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-tauri-version", + "markdownDescription": "Enables the tauri_version command without any pre-configured scope." + }, + { + "description": "Enables the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-version", + "markdownDescription": "Enables the version command without any pre-configured scope." + }, + { + "description": "Denies the app_hide command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-hide", + "markdownDescription": "Denies the app_hide command without any pre-configured scope." + }, + { + "description": "Denies the app_show command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-app-show", + "markdownDescription": "Denies the app_show command without any pre-configured scope." + }, + { + "description": "Denies the bundle_type command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-bundle-type", + "markdownDescription": "Denies the bundle_type command without any pre-configured scope." + }, + { + "description": "Denies the default_window_icon command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-default-window-icon", + "markdownDescription": "Denies the default_window_icon command without any pre-configured scope." + }, + { + "description": "Denies the exit command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-exit", + "markdownDescription": "Denies the exit command without any pre-configured scope." + }, + { + "description": "Denies the fetch_data_store_identifiers command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-fetch-data-store-identifiers", + "markdownDescription": "Denies the fetch_data_store_identifiers command without any pre-configured scope." + }, + { + "description": "Denies the identifier command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-identifier", + "markdownDescription": "Denies the identifier command without any pre-configured scope." + }, + { + "description": "Denies the name command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-name", + "markdownDescription": "Denies the name command without any pre-configured scope." + }, + { + "description": "Denies the register_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-register-listener", + "markdownDescription": "Denies the register_listener command without any pre-configured scope." + }, + { + "description": "Denies the remove_data_store command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-data-store", + "markdownDescription": "Denies the remove_data_store command without any pre-configured scope." + }, + { + "description": "Denies the remove_listener command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-remove-listener", + "markdownDescription": "Denies the remove_listener command without any pre-configured scope." + }, + { + "description": "Denies the set_app_theme command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-app-theme", + "markdownDescription": "Denies the set_app_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_dock_visibility command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-set-dock-visibility", + "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." + }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, + { + "description": "Denies the tauri_version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-tauri-version", + "markdownDescription": "Denies the tauri_version command without any pre-configured scope." + }, + { + "description": "Denies the version command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-version", + "markdownDescription": "Denies the version command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`", + "type": "string", + "const": "core:event:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-listen`\n- `allow-unlisten`\n- `allow-emit`\n- `allow-emit-to`" + }, + { + "description": "Enables the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit", + "markdownDescription": "Enables the emit command without any pre-configured scope." + }, + { + "description": "Enables the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-emit-to", + "markdownDescription": "Enables the emit_to command without any pre-configured scope." + }, + { + "description": "Enables the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-listen", + "markdownDescription": "Enables the listen command without any pre-configured scope." + }, + { + "description": "Enables the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:allow-unlisten", + "markdownDescription": "Enables the unlisten command without any pre-configured scope." + }, + { + "description": "Denies the emit command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit", + "markdownDescription": "Denies the emit command without any pre-configured scope." + }, + { + "description": "Denies the emit_to command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-emit-to", + "markdownDescription": "Denies the emit_to command without any pre-configured scope." + }, + { + "description": "Denies the listen command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-listen", + "markdownDescription": "Denies the listen command without any pre-configured scope." + }, + { + "description": "Denies the unlisten command without any pre-configured scope.", + "type": "string", + "const": "core:event:deny-unlisten", + "markdownDescription": "Denies the unlisten command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`", + "type": "string", + "const": "core:image:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-from-bytes`\n- `allow-from-path`\n- `allow-rgba`\n- `allow-size`" + }, + { + "description": "Enables the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-bytes", + "markdownDescription": "Enables the from_bytes command without any pre-configured scope." + }, + { + "description": "Enables the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-from-path", + "markdownDescription": "Enables the from_path command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-rgba", + "markdownDescription": "Enables the rgba command without any pre-configured scope." + }, + { + "description": "Enables the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:allow-size", + "markdownDescription": "Enables the size command without any pre-configured scope." + }, + { + "description": "Denies the from_bytes command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-bytes", + "markdownDescription": "Denies the from_bytes command without any pre-configured scope." + }, + { + "description": "Denies the from_path command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-from-path", + "markdownDescription": "Denies the from_path command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the rgba command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-rgba", + "markdownDescription": "Denies the rgba command without any pre-configured scope." + }, + { + "description": "Denies the size command without any pre-configured scope.", + "type": "string", + "const": "core:image:deny-size", + "markdownDescription": "Denies the size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`", + "type": "string", + "const": "core:menu:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-append`\n- `allow-prepend`\n- `allow-insert`\n- `allow-remove`\n- `allow-remove-at`\n- `allow-items`\n- `allow-get`\n- `allow-popup`\n- `allow-create-default`\n- `allow-set-as-app-menu`\n- `allow-set-as-window-menu`\n- `allow-text`\n- `allow-set-text`\n- `allow-is-enabled`\n- `allow-set-enabled`\n- `allow-set-accelerator`\n- `allow-set-as-windows-menu-for-nsapp`\n- `allow-set-as-help-menu-for-nsapp`\n- `allow-is-checked`\n- `allow-set-checked`\n- `allow-set-icon`" + }, + { + "description": "Enables the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-append", + "markdownDescription": "Enables the append command without any pre-configured scope." + }, + { + "description": "Enables the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-create-default", + "markdownDescription": "Enables the create_default command without any pre-configured scope." + }, + { + "description": "Enables the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-get", + "markdownDescription": "Enables the get command without any pre-configured scope." + }, + { + "description": "Enables the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-insert", + "markdownDescription": "Enables the insert command without any pre-configured scope." + }, + { + "description": "Enables the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-checked", + "markdownDescription": "Enables the is_checked command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-items", + "markdownDescription": "Enables the items command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-popup", + "markdownDescription": "Enables the popup command without any pre-configured scope." + }, + { + "description": "Enables the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-prepend", + "markdownDescription": "Enables the prepend command without any pre-configured scope." + }, + { + "description": "Enables the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove", + "markdownDescription": "Enables the remove command without any pre-configured scope." + }, + { + "description": "Enables the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-remove-at", + "markdownDescription": "Enables the remove_at command without any pre-configured scope." + }, + { + "description": "Enables the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-accelerator", + "markdownDescription": "Enables the set_accelerator command without any pre-configured scope." + }, + { + "description": "Enables the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-app-menu", + "markdownDescription": "Enables the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-help-menu-for-nsapp", + "markdownDescription": "Enables the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-window-menu", + "markdownDescription": "Enables the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-as-windows-menu-for-nsapp", + "markdownDescription": "Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Enables the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-checked", + "markdownDescription": "Enables the set_checked command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-set-text", + "markdownDescription": "Enables the set_text command without any pre-configured scope." + }, + { + "description": "Enables the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:allow-text", + "markdownDescription": "Enables the text command without any pre-configured scope." + }, + { + "description": "Denies the append command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-append", + "markdownDescription": "Denies the append command without any pre-configured scope." + }, + { + "description": "Denies the create_default command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-create-default", + "markdownDescription": "Denies the create_default command without any pre-configured scope." + }, + { + "description": "Denies the get command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-get", + "markdownDescription": "Denies the get command without any pre-configured scope." + }, + { + "description": "Denies the insert command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-insert", + "markdownDescription": "Denies the insert command without any pre-configured scope." + }, + { + "description": "Denies the is_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-checked", + "markdownDescription": "Denies the is_checked command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the items command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-items", + "markdownDescription": "Denies the items command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the popup command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-popup", + "markdownDescription": "Denies the popup command without any pre-configured scope." + }, + { + "description": "Denies the prepend command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-prepend", + "markdownDescription": "Denies the prepend command without any pre-configured scope." + }, + { + "description": "Denies the remove command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove", + "markdownDescription": "Denies the remove command without any pre-configured scope." + }, + { + "description": "Denies the remove_at command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-remove-at", + "markdownDescription": "Denies the remove_at command without any pre-configured scope." + }, + { + "description": "Denies the set_accelerator command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-accelerator", + "markdownDescription": "Denies the set_accelerator command without any pre-configured scope." + }, + { + "description": "Denies the set_as_app_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-app-menu", + "markdownDescription": "Denies the set_as_app_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-help-menu-for-nsapp", + "markdownDescription": "Denies the set_as_help_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_as_window_menu command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-window-menu", + "markdownDescription": "Denies the set_as_window_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-as-windows-menu-for-nsapp", + "markdownDescription": "Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope." + }, + { + "description": "Denies the set_checked command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-checked", + "markdownDescription": "Denies the set_checked command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-set-text", + "markdownDescription": "Denies the set_text command without any pre-configured scope." + }, + { + "description": "Denies the text command without any pre-configured scope.", + "type": "string", + "const": "core:menu:deny-text", + "markdownDescription": "Denies the text command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`", + "type": "string", + "const": "core:path:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-resolve-directory`\n- `allow-resolve`\n- `allow-normalize`\n- `allow-join`\n- `allow-dirname`\n- `allow-extname`\n- `allow-basename`\n- `allow-is-absolute`" + }, + { + "description": "Enables the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-basename", + "markdownDescription": "Enables the basename command without any pre-configured scope." + }, + { + "description": "Enables the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-dirname", + "markdownDescription": "Enables the dirname command without any pre-configured scope." + }, + { + "description": "Enables the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-extname", + "markdownDescription": "Enables the extname command without any pre-configured scope." + }, + { + "description": "Enables the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-is-absolute", + "markdownDescription": "Enables the is_absolute command without any pre-configured scope." + }, + { + "description": "Enables the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-join", + "markdownDescription": "Enables the join command without any pre-configured scope." + }, + { + "description": "Enables the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-normalize", + "markdownDescription": "Enables the normalize command without any pre-configured scope." + }, + { + "description": "Enables the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve", + "markdownDescription": "Enables the resolve command without any pre-configured scope." + }, + { + "description": "Enables the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:allow-resolve-directory", + "markdownDescription": "Enables the resolve_directory command without any pre-configured scope." + }, + { + "description": "Denies the basename command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-basename", + "markdownDescription": "Denies the basename command without any pre-configured scope." + }, + { + "description": "Denies the dirname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-dirname", + "markdownDescription": "Denies the dirname command without any pre-configured scope." + }, + { + "description": "Denies the extname command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-extname", + "markdownDescription": "Denies the extname command without any pre-configured scope." + }, + { + "description": "Denies the is_absolute command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-is-absolute", + "markdownDescription": "Denies the is_absolute command without any pre-configured scope." + }, + { + "description": "Denies the join command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-join", + "markdownDescription": "Denies the join command without any pre-configured scope." + }, + { + "description": "Denies the normalize command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-normalize", + "markdownDescription": "Denies the normalize command without any pre-configured scope." + }, + { + "description": "Denies the resolve command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve", + "markdownDescription": "Denies the resolve command without any pre-configured scope." + }, + { + "description": "Denies the resolve_directory command without any pre-configured scope.", + "type": "string", + "const": "core:path:deny-resolve-directory", + "markdownDescription": "Denies the resolve_directory command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`", + "type": "string", + "const": "core:resources:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-close`" + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:resources:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", + "type": "string", + "const": "core:tray:default", + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" + }, + { + "description": "Enables the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-get-by-id", + "markdownDescription": "Enables the get_by_id command without any pre-configured scope." + }, + { + "description": "Enables the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-new", + "markdownDescription": "Enables the new command without any pre-configured scope." + }, + { + "description": "Enables the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-remove-by-id", + "markdownDescription": "Enables the remove_by_id command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-as-template", + "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Enables the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-menu", + "markdownDescription": "Enables the set_menu command without any pre-configured scope." + }, + { + "description": "Enables the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-show-menu-on-left-click", + "markdownDescription": "Enables the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Enables the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-temp-dir-path", + "markdownDescription": "Enables the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-tooltip", + "markdownDescription": "Enables the set_tooltip command without any pre-configured scope." + }, + { + "description": "Enables the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-visible", + "markdownDescription": "Enables the set_visible command without any pre-configured scope." + }, + { + "description": "Denies the get_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-get-by-id", + "markdownDescription": "Denies the get_by_id command without any pre-configured scope." + }, + { + "description": "Denies the new command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-new", + "markdownDescription": "Denies the new command without any pre-configured scope." + }, + { + "description": "Denies the remove_by_id command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-remove-by-id", + "markdownDescription": "Denies the remove_by_id command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-as-template", + "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, + { + "description": "Denies the set_menu command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-menu", + "markdownDescription": "Denies the set_menu command without any pre-configured scope." + }, + { + "description": "Denies the set_show_menu_on_left_click command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-show-menu-on-left-click", + "markdownDescription": "Denies the set_show_menu_on_left_click command without any pre-configured scope." + }, + { + "description": "Denies the set_temp_dir_path command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-temp-dir-path", + "markdownDescription": "Denies the set_temp_dir_path command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_tooltip command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-tooltip", + "markdownDescription": "Denies the set_tooltip command without any pre-configured scope." + }, + { + "description": "Denies the set_visible command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-visible", + "markdownDescription": "Denies the set_visible command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`", + "type": "string", + "const": "core:webview:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-webviews`\n- `allow-webview-position`\n- `allow-webview-size`\n- `allow-internal-toggle-devtools`" + }, + { + "description": "Enables the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-clear-all-browsing-data", + "markdownDescription": "Enables the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Enables the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview", + "markdownDescription": "Enables the create_webview command without any pre-configured scope." + }, + { + "description": "Enables the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-create-webview-window", + "markdownDescription": "Enables the create_webview_window command without any pre-configured scope." + }, + { + "description": "Enables the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-get-all-webviews", + "markdownDescription": "Enables the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-internal-toggle-devtools", + "markdownDescription": "Enables the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Enables the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-print", + "markdownDescription": "Enables the print command without any pre-configured scope." + }, + { + "description": "Enables the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-reparent", + "markdownDescription": "Enables the reparent command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-auto-resize", + "markdownDescription": "Enables the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-background-color", + "markdownDescription": "Enables the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-focus", + "markdownDescription": "Enables the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-position", + "markdownDescription": "Enables the set_webview_position command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-size", + "markdownDescription": "Enables the set_webview_size command without any pre-configured scope." + }, + { + "description": "Enables the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-set-webview-zoom", + "markdownDescription": "Enables the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Enables the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-close", + "markdownDescription": "Enables the webview_close command without any pre-configured scope." + }, + { + "description": "Enables the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-hide", + "markdownDescription": "Enables the webview_hide command without any pre-configured scope." + }, + { + "description": "Enables the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-position", + "markdownDescription": "Enables the webview_position command without any pre-configured scope." + }, + { + "description": "Enables the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-show", + "markdownDescription": "Enables the webview_show command without any pre-configured scope." + }, + { + "description": "Enables the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:allow-webview-size", + "markdownDescription": "Enables the webview_size command without any pre-configured scope." + }, + { + "description": "Denies the clear_all_browsing_data command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-clear-all-browsing-data", + "markdownDescription": "Denies the clear_all_browsing_data command without any pre-configured scope." + }, + { + "description": "Denies the create_webview command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview", + "markdownDescription": "Denies the create_webview command without any pre-configured scope." + }, + { + "description": "Denies the create_webview_window command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-create-webview-window", + "markdownDescription": "Denies the create_webview_window command without any pre-configured scope." + }, + { + "description": "Denies the get_all_webviews command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-get-all-webviews", + "markdownDescription": "Denies the get_all_webviews command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_devtools command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-internal-toggle-devtools", + "markdownDescription": "Denies the internal_toggle_devtools command without any pre-configured scope." + }, + { + "description": "Denies the print command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-print", + "markdownDescription": "Denies the print command without any pre-configured scope." + }, + { + "description": "Denies the reparent command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-reparent", + "markdownDescription": "Denies the reparent command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_auto_resize command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-auto-resize", + "markdownDescription": "Denies the set_webview_auto_resize command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-background-color", + "markdownDescription": "Denies the set_webview_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_focus command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-focus", + "markdownDescription": "Denies the set_webview_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-position", + "markdownDescription": "Denies the set_webview_position command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-size", + "markdownDescription": "Denies the set_webview_size command without any pre-configured scope." + }, + { + "description": "Denies the set_webview_zoom command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-set-webview-zoom", + "markdownDescription": "Denies the set_webview_zoom command without any pre-configured scope." + }, + { + "description": "Denies the webview_close command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-close", + "markdownDescription": "Denies the webview_close command without any pre-configured scope." + }, + { + "description": "Denies the webview_hide command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-hide", + "markdownDescription": "Denies the webview_hide command without any pre-configured scope." + }, + { + "description": "Denies the webview_position command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-position", + "markdownDescription": "Denies the webview_position command without any pre-configured scope." + }, + { + "description": "Denies the webview_show command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-show", + "markdownDescription": "Denies the webview_show command without any pre-configured scope." + }, + { + "description": "Denies the webview_size command without any pre-configured scope.", + "type": "string", + "const": "core:webview:deny-webview-size", + "markdownDescription": "Denies the webview_size command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", + "type": "string", + "const": "core:window:default", + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." + }, + { + "description": "Enables the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-available-monitors", + "markdownDescription": "Enables the available_monitors command without any pre-configured scope." + }, + { + "description": "Enables the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-center", + "markdownDescription": "Enables the center command without any pre-configured scope." + }, + { + "description": "Enables the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-close", + "markdownDescription": "Enables the close command without any pre-configured scope." + }, + { + "description": "Enables the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-create", + "markdownDescription": "Enables the create command without any pre-configured scope." + }, + { + "description": "Enables the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-current-monitor", + "markdownDescription": "Enables the current_monitor command without any pre-configured scope." + }, + { + "description": "Enables the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-cursor-position", + "markdownDescription": "Enables the cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-destroy", + "markdownDescription": "Enables the destroy command without any pre-configured scope." + }, + { + "description": "Enables the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-get-all-windows", + "markdownDescription": "Enables the get_all_windows command without any pre-configured scope." + }, + { + "description": "Enables the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-hide", + "markdownDescription": "Enables the hide command without any pre-configured scope." + }, + { + "description": "Enables the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-position", + "markdownDescription": "Enables the inner_position command without any pre-configured scope." + }, + { + "description": "Enables the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-inner-size", + "markdownDescription": "Enables the inner_size command without any pre-configured scope." + }, + { + "description": "Enables the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-internal-toggle-maximize", + "markdownDescription": "Enables the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-always-on-top", + "markdownDescription": "Enables the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-closable", + "markdownDescription": "Enables the is_closable command without any pre-configured scope." + }, + { + "description": "Enables the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-decorated", + "markdownDescription": "Enables the is_decorated command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Enables the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-focused", + "markdownDescription": "Enables the is_focused command without any pre-configured scope." + }, + { + "description": "Enables the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-fullscreen", + "markdownDescription": "Enables the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximizable", + "markdownDescription": "Enables the is_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-maximized", + "markdownDescription": "Enables the is_maximized command without any pre-configured scope." + }, + { + "description": "Enables the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimizable", + "markdownDescription": "Enables the is_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-minimized", + "markdownDescription": "Enables the is_minimized command without any pre-configured scope." + }, + { + "description": "Enables the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-resizable", + "markdownDescription": "Enables the is_resizable command without any pre-configured scope." + }, + { + "description": "Enables the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-is-visible", + "markdownDescription": "Enables the is_visible command without any pre-configured scope." + }, + { + "description": "Enables the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-maximize", + "markdownDescription": "Enables the maximize command without any pre-configured scope." + }, + { + "description": "Enables the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-minimize", + "markdownDescription": "Enables the minimize command without any pre-configured scope." + }, + { + "description": "Enables the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-monitor-from-point", + "markdownDescription": "Enables the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Enables the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-position", + "markdownDescription": "Enables the outer_position command without any pre-configured scope." + }, + { + "description": "Enables the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-outer-size", + "markdownDescription": "Enables the outer_size command without any pre-configured scope." + }, + { + "description": "Enables the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-primary-monitor", + "markdownDescription": "Enables the primary_monitor command without any pre-configured scope." + }, + { + "description": "Enables the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-request-user-attention", + "markdownDescription": "Enables the request_user_attention command without any pre-configured scope." + }, + { + "description": "Enables the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scale-factor", + "markdownDescription": "Enables the scale_factor command without any pre-configured scope." + }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-bottom", + "markdownDescription": "Enables the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Enables the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-always-on-top", + "markdownDescription": "Enables the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Enables the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-background-color", + "markdownDescription": "Enables the set_background_color command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-count", + "markdownDescription": "Enables the set_badge_count command without any pre-configured scope." + }, + { + "description": "Enables the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-badge-label", + "markdownDescription": "Enables the set_badge_label command without any pre-configured scope." + }, + { + "description": "Enables the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-closable", + "markdownDescription": "Enables the set_closable command without any pre-configured scope." + }, + { + "description": "Enables the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-content-protected", + "markdownDescription": "Enables the set_content_protected command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-grab", + "markdownDescription": "Enables the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-icon", + "markdownDescription": "Enables the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-position", + "markdownDescription": "Enables the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Enables the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-cursor-visible", + "markdownDescription": "Enables the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Enables the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-decorations", + "markdownDescription": "Enables the set_decorations command without any pre-configured scope." + }, + { + "description": "Enables the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-effects", + "markdownDescription": "Enables the set_effects command without any pre-configured scope." + }, + { + "description": "Enables the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-enabled", + "markdownDescription": "Enables the set_enabled command without any pre-configured scope." + }, + { + "description": "Enables the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focus", + "markdownDescription": "Enables the set_focus command without any pre-configured scope." + }, + { + "description": "Enables the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-focusable", + "markdownDescription": "Enables the set_focusable command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen", + "markdownDescription": "Enables the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-fullscreen-on-monitor", + "markdownDescription": "Enables the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Enables the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-icon", + "markdownDescription": "Enables the set_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-ignore-cursor-events", + "markdownDescription": "Enables the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Enables the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-max-size", + "markdownDescription": "Enables the set_max_size command without any pre-configured scope." + }, + { + "description": "Enables the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-maximizable", + "markdownDescription": "Enables the set_maximizable command without any pre-configured scope." + }, + { + "description": "Enables the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-min-size", + "markdownDescription": "Enables the set_min_size command without any pre-configured scope." + }, + { + "description": "Enables the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-minimizable", + "markdownDescription": "Enables the set_minimizable command without any pre-configured scope." + }, + { + "description": "Enables the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-overlay-icon", + "markdownDescription": "Enables the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Enables the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-position", + "markdownDescription": "Enables the set_position command without any pre-configured scope." + }, + { + "description": "Enables the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-progress-bar", + "markdownDescription": "Enables the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Enables the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-resizable", + "markdownDescription": "Enables the set_resizable command without any pre-configured scope." + }, + { + "description": "Enables the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-shadow", + "markdownDescription": "Enables the set_shadow command without any pre-configured scope." + }, + { + "description": "Enables the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-simple-fullscreen", + "markdownDescription": "Enables the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Enables the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size", + "markdownDescription": "Enables the set_size command without any pre-configured scope." + }, + { + "description": "Enables the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-size-constraints", + "markdownDescription": "Enables the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Enables the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-skip-taskbar", + "markdownDescription": "Enables the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Enables the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-theme", + "markdownDescription": "Enables the set_theme command without any pre-configured scope." + }, + { + "description": "Enables the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title", + "markdownDescription": "Enables the set_title command without any pre-configured scope." + }, + { + "description": "Enables the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-title-bar-style", + "markdownDescription": "Enables the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Enables the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-set-visible-on-all-workspaces", + "markdownDescription": "Enables the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Enables the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-show", + "markdownDescription": "Enables the show command without any pre-configured scope." + }, + { + "description": "Enables the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-dragging", + "markdownDescription": "Enables the start_dragging command without any pre-configured scope." + }, + { + "description": "Enables the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-start-resize-dragging", + "markdownDescription": "Enables the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Enables the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-theme", + "markdownDescription": "Enables the theme command without any pre-configured scope." + }, + { + "description": "Enables the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-title", + "markdownDescription": "Enables the title command without any pre-configured scope." + }, + { + "description": "Enables the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-toggle-maximize", + "markdownDescription": "Enables the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Enables the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unmaximize", + "markdownDescription": "Enables the unmaximize command without any pre-configured scope." + }, + { + "description": "Enables the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-unminimize", + "markdownDescription": "Enables the unminimize command without any pre-configured scope." + }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, + { + "description": "Denies the available_monitors command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-available-monitors", + "markdownDescription": "Denies the available_monitors command without any pre-configured scope." + }, + { + "description": "Denies the center command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-center", + "markdownDescription": "Denies the center command without any pre-configured scope." + }, + { + "description": "Denies the close command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-close", + "markdownDescription": "Denies the close command without any pre-configured scope." + }, + { + "description": "Denies the create command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-create", + "markdownDescription": "Denies the create command without any pre-configured scope." + }, + { + "description": "Denies the current_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-current-monitor", + "markdownDescription": "Denies the current_monitor command without any pre-configured scope." + }, + { + "description": "Denies the cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-cursor-position", + "markdownDescription": "Denies the cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the destroy command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-destroy", + "markdownDescription": "Denies the destroy command without any pre-configured scope." + }, + { + "description": "Denies the get_all_windows command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-get-all-windows", + "markdownDescription": "Denies the get_all_windows command without any pre-configured scope." + }, + { + "description": "Denies the hide command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-hide", + "markdownDescription": "Denies the hide command without any pre-configured scope." + }, + { + "description": "Denies the inner_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-position", + "markdownDescription": "Denies the inner_position command without any pre-configured scope." + }, + { + "description": "Denies the inner_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-inner-size", + "markdownDescription": "Denies the inner_size command without any pre-configured scope." + }, + { + "description": "Denies the internal_toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-internal-toggle-maximize", + "markdownDescription": "Denies the internal_toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the is_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-always-on-top", + "markdownDescription": "Denies the is_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the is_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-closable", + "markdownDescription": "Denies the is_closable command without any pre-configured scope." + }, + { + "description": "Denies the is_decorated command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-decorated", + "markdownDescription": "Denies the is_decorated command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the is_focused command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-focused", + "markdownDescription": "Denies the is_focused command without any pre-configured scope." + }, + { + "description": "Denies the is_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-fullscreen", + "markdownDescription": "Denies the is_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the is_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximizable", + "markdownDescription": "Denies the is_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the is_maximized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-maximized", + "markdownDescription": "Denies the is_maximized command without any pre-configured scope." + }, + { + "description": "Denies the is_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimizable", + "markdownDescription": "Denies the is_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the is_minimized command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-minimized", + "markdownDescription": "Denies the is_minimized command without any pre-configured scope." + }, + { + "description": "Denies the is_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-resizable", + "markdownDescription": "Denies the is_resizable command without any pre-configured scope." + }, + { + "description": "Denies the is_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-is-visible", + "markdownDescription": "Denies the is_visible command without any pre-configured scope." + }, + { + "description": "Denies the maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-maximize", + "markdownDescription": "Denies the maximize command without any pre-configured scope." + }, + { + "description": "Denies the minimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-minimize", + "markdownDescription": "Denies the minimize command without any pre-configured scope." + }, + { + "description": "Denies the monitor_from_point command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-monitor-from-point", + "markdownDescription": "Denies the monitor_from_point command without any pre-configured scope." + }, + { + "description": "Denies the outer_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-position", + "markdownDescription": "Denies the outer_position command without any pre-configured scope." + }, + { + "description": "Denies the outer_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-outer-size", + "markdownDescription": "Denies the outer_size command without any pre-configured scope." + }, + { + "description": "Denies the primary_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-primary-monitor", + "markdownDescription": "Denies the primary_monitor command without any pre-configured scope." + }, + { + "description": "Denies the request_user_attention command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-request-user-attention", + "markdownDescription": "Denies the request_user_attention command without any pre-configured scope." + }, + { + "description": "Denies the scale_factor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scale-factor", + "markdownDescription": "Denies the scale_factor command without any pre-configured scope." + }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_bottom command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-bottom", + "markdownDescription": "Denies the set_always_on_bottom command without any pre-configured scope." + }, + { + "description": "Denies the set_always_on_top command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-always-on-top", + "markdownDescription": "Denies the set_always_on_top command without any pre-configured scope." + }, + { + "description": "Denies the set_background_color command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-background-color", + "markdownDescription": "Denies the set_background_color command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_count command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-count", + "markdownDescription": "Denies the set_badge_count command without any pre-configured scope." + }, + { + "description": "Denies the set_badge_label command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-badge-label", + "markdownDescription": "Denies the set_badge_label command without any pre-configured scope." + }, + { + "description": "Denies the set_closable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-closable", + "markdownDescription": "Denies the set_closable command without any pre-configured scope." + }, + { + "description": "Denies the set_content_protected command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-content-protected", + "markdownDescription": "Denies the set_content_protected command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_grab command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-grab", + "markdownDescription": "Denies the set_cursor_grab command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-icon", + "markdownDescription": "Denies the set_cursor_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-position", + "markdownDescription": "Denies the set_cursor_position command without any pre-configured scope." + }, + { + "description": "Denies the set_cursor_visible command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-cursor-visible", + "markdownDescription": "Denies the set_cursor_visible command without any pre-configured scope." + }, + { + "description": "Denies the set_decorations command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-decorations", + "markdownDescription": "Denies the set_decorations command without any pre-configured scope." + }, + { + "description": "Denies the set_effects command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-effects", + "markdownDescription": "Denies the set_effects command without any pre-configured scope." + }, + { + "description": "Denies the set_enabled command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-enabled", + "markdownDescription": "Denies the set_enabled command without any pre-configured scope." + }, + { + "description": "Denies the set_focus command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focus", + "markdownDescription": "Denies the set_focus command without any pre-configured scope." + }, + { + "description": "Denies the set_focusable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-focusable", + "markdownDescription": "Denies the set_focusable command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen", + "markdownDescription": "Denies the set_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_fullscreen_on_monitor command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-fullscreen-on-monitor", + "markdownDescription": "Denies the set_fullscreen_on_monitor command without any pre-configured scope." + }, + { + "description": "Denies the set_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-icon", + "markdownDescription": "Denies the set_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_ignore_cursor_events command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-ignore-cursor-events", + "markdownDescription": "Denies the set_ignore_cursor_events command without any pre-configured scope." + }, + { + "description": "Denies the set_max_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-max-size", + "markdownDescription": "Denies the set_max_size command without any pre-configured scope." + }, + { + "description": "Denies the set_maximizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-maximizable", + "markdownDescription": "Denies the set_maximizable command without any pre-configured scope." + }, + { + "description": "Denies the set_min_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-min-size", + "markdownDescription": "Denies the set_min_size command without any pre-configured scope." + }, + { + "description": "Denies the set_minimizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-minimizable", + "markdownDescription": "Denies the set_minimizable command without any pre-configured scope." + }, + { + "description": "Denies the set_overlay_icon command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-overlay-icon", + "markdownDescription": "Denies the set_overlay_icon command without any pre-configured scope." + }, + { + "description": "Denies the set_position command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-position", + "markdownDescription": "Denies the set_position command without any pre-configured scope." + }, + { + "description": "Denies the set_progress_bar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-progress-bar", + "markdownDescription": "Denies the set_progress_bar command without any pre-configured scope." + }, + { + "description": "Denies the set_resizable command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-resizable", + "markdownDescription": "Denies the set_resizable command without any pre-configured scope." + }, + { + "description": "Denies the set_shadow command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-shadow", + "markdownDescription": "Denies the set_shadow command without any pre-configured scope." + }, + { + "description": "Denies the set_simple_fullscreen command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-simple-fullscreen", + "markdownDescription": "Denies the set_simple_fullscreen command without any pre-configured scope." + }, + { + "description": "Denies the set_size command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size", + "markdownDescription": "Denies the set_size command without any pre-configured scope." + }, + { + "description": "Denies the set_size_constraints command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-size-constraints", + "markdownDescription": "Denies the set_size_constraints command without any pre-configured scope." + }, + { + "description": "Denies the set_skip_taskbar command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-skip-taskbar", + "markdownDescription": "Denies the set_skip_taskbar command without any pre-configured scope." + }, + { + "description": "Denies the set_theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-theme", + "markdownDescription": "Denies the set_theme command without any pre-configured scope." + }, + { + "description": "Denies the set_title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title", + "markdownDescription": "Denies the set_title command without any pre-configured scope." + }, + { + "description": "Denies the set_title_bar_style command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-title-bar-style", + "markdownDescription": "Denies the set_title_bar_style command without any pre-configured scope." + }, + { + "description": "Denies the set_visible_on_all_workspaces command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-set-visible-on-all-workspaces", + "markdownDescription": "Denies the set_visible_on_all_workspaces command without any pre-configured scope." + }, + { + "description": "Denies the show command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-show", + "markdownDescription": "Denies the show command without any pre-configured scope." + }, + { + "description": "Denies the start_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-dragging", + "markdownDescription": "Denies the start_dragging command without any pre-configured scope." + }, + { + "description": "Denies the start_resize_dragging command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-start-resize-dragging", + "markdownDescription": "Denies the start_resize_dragging command without any pre-configured scope." + }, + { + "description": "Denies the theme command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-theme", + "markdownDescription": "Denies the theme command without any pre-configured scope." + }, + { + "description": "Denies the title command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-title", + "markdownDescription": "Denies the title command without any pre-configured scope." + }, + { + "description": "Denies the toggle_maximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-toggle-maximize", + "markdownDescription": "Denies the toggle_maximize command without any pre-configured scope." + }, + { + "description": "Denies the unmaximize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unmaximize", + "markdownDescription": "Denies the unmaximize command without any pre-configured scope." + }, + { + "description": "Denies the unminimize command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-unminimize", + "markdownDescription": "Denies the unminimize command without any pre-configured scope." + } + ] + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + } + } +} \ No newline at end of file diff --git a/apps/gui/src-tauri/icons/128x128.png b/apps/gui/src-tauri/icons/128x128.png new file mode 100644 index 0000000..b2a5014 Binary files /dev/null and b/apps/gui/src-tauri/icons/128x128.png differ diff --git a/apps/gui/src-tauri/icons/128x128@2x.png b/apps/gui/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..1954fe6 Binary files /dev/null and b/apps/gui/src-tauri/icons/128x128@2x.png differ diff --git a/apps/gui/src-tauri/icons/32x32.png b/apps/gui/src-tauri/icons/32x32.png new file mode 100644 index 0000000..9cc4294 Binary files /dev/null and b/apps/gui/src-tauri/icons/32x32.png differ diff --git a/apps/gui/src-tauri/icons/64x64.png b/apps/gui/src-tauri/icons/64x64.png new file mode 100644 index 0000000..de49925 Binary files /dev/null and b/apps/gui/src-tauri/icons/64x64.png differ diff --git a/apps/gui/src-tauri/icons/Square107x107Logo.png b/apps/gui/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..4d5a7a8 Binary files /dev/null and b/apps/gui/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square142x142Logo.png b/apps/gui/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..400eca4 Binary files /dev/null and b/apps/gui/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square150x150Logo.png b/apps/gui/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..b473d51 Binary files /dev/null and b/apps/gui/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square284x284Logo.png b/apps/gui/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..445ee80 Binary files /dev/null and b/apps/gui/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square30x30Logo.png b/apps/gui/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..90f9b6d Binary files /dev/null and b/apps/gui/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square310x310Logo.png b/apps/gui/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..3f9f83a Binary files /dev/null and b/apps/gui/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square44x44Logo.png b/apps/gui/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..ec500c0 Binary files /dev/null and b/apps/gui/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square71x71Logo.png b/apps/gui/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..2c39727 Binary files /dev/null and b/apps/gui/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/gui/src-tauri/icons/Square89x89Logo.png b/apps/gui/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..a85645f Binary files /dev/null and b/apps/gui/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/gui/src-tauri/icons/StoreLogo.png b/apps/gui/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..1acbd6e Binary files /dev/null and b/apps/gui/src-tauri/icons/StoreLogo.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/apps/gui/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/apps/gui/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..39afdb8 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..09c8af9 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..5a97369 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..8b383d0 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a775594 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..9b34bcc Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6d59026 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..3e48573 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e369634 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..e20cad5 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f97abf4 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..70b711b Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..5f60d3f Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..427da81 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..57347d0 Binary files /dev/null and b/apps/gui/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/gui/src-tauri/icons/android/values/ic_launcher_background.xml b/apps/gui/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..16cdd5a --- /dev/null +++ b/apps/gui/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/apps/gui/src-tauri/icons/icon.icns b/apps/gui/src-tauri/icons/icon.icns new file mode 100644 index 0000000..1b5300a Binary files /dev/null and b/apps/gui/src-tauri/icons/icon.icns differ diff --git a/apps/gui/src-tauri/icons/icon.ico b/apps/gui/src-tauri/icons/icon.ico new file mode 100644 index 0000000..e8e6543 Binary files /dev/null and b/apps/gui/src-tauri/icons/icon.ico differ diff --git a/apps/gui/src-tauri/icons/icon.png b/apps/gui/src-tauri/icons/icon.png new file mode 100644 index 0000000..c1180f6 Binary files /dev/null and b/apps/gui/src-tauri/icons/icon.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-20x20@1x.png b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..2f2a583 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..4bc8388 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-20x20@2x.png b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..4bc8388 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-20x20@3x.png b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..f9644fa Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-29x29@1x.png b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..01a1a05 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..a3209f5 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-29x29@2x.png b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..a3209f5 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-29x29@3x.png b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..79070e3 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-40x40@1x.png b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..4bc8388 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..d8ee318 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-40x40@2x.png b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..d8ee318 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-40x40@3x.png b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..5ab941f Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-512@2x.png b/apps/gui/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..faff0a5 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-60x60@2x.png b/apps/gui/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..5ab941f Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-60x60@3x.png b/apps/gui/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..06ca7ee Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-76x76@1x.png b/apps/gui/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..63417a5 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-76x76@2x.png b/apps/gui/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..89963fe Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/apps/gui/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/apps/gui/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..0f8a403 Binary files /dev/null and b/apps/gui/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/apps/gui/src-tauri/icons/source.png b/apps/gui/src-tauri/icons/source.png index 0b20ffb..809e54f 100644 Binary files a/apps/gui/src-tauri/icons/source.png and b/apps/gui/src-tauri/icons/source.png differ diff --git a/apps/gui/ui/app.css b/apps/gui/ui/app.css index 4e7bae4..6a99237 100644 --- a/apps/gui/ui/app.css +++ b/apps/gui/ui/app.css @@ -113,6 +113,8 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; } .side-foot { margin-top: auto; padding: 14px 10px 4px; font-size: 11.5px; color: var(--text-3); display: flex; justify-content: space-between; gap: 8px; } .side-foot button { background: transparent; border: 0; color: var(--text-3); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; } .side-foot button:hover { color: var(--text-2); } +.lang-sel { background: transparent; border: 0; color: var(--text-3); font-size: 11.5px; cursor: pointer; } +.lang-sel:hover { color: var(--text-2); } .main { overflow-y: auto; min-width: 0; } .page { max-width: 1040px; margin: 0 auto; padding: 30px 36px 100px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); } diff --git a/apps/gui/ui/app.js b/apps/gui/ui/app.js index 5020f77..122b721 100644 --- a/apps/gui/ui/app.js +++ b/apps/gui/ui/app.js @@ -1,3 +1,5 @@ +import { setLang, currentLang } from './i18n.js'; + const params = new URLSearchParams(location.search); const token = params.get('token'); const tauri = window.__TAURI__; @@ -14,8 +16,22 @@ const els = { wave: document.getElementById('wave'), verInfo: document.getElementById('verInfo'), btnUpdate: document.getElementById('btnUpdate'), + langSel: document.getElementById('langSel'), }; +function applyLangPref() { + let pref = 'auto'; + try { pref = localStorage.getItem('sabia-lang') || 'auto'; } catch { /* ignora */ } + els.langSel.value = pref; + setLang(pref === 'auto' ? undefined : pref); +} + +els.langSel.addEventListener('change', () => { + const pref = els.langSel.value; + try { localStorage.setItem('sabia-lang', pref); } catch { /* ignora */ } + setLang(pref === 'auto' ? undefined : pref); +}); + let ifaces = []; let current = null; let scanning = false; @@ -248,6 +264,7 @@ async function checkElevation() { } (async function init() { + applyLangPref(); checkElevation(); const status = await api('/api/version'); els.verInfo.textContent = `v${status.version}`; diff --git a/apps/gui/ui/i18n-en.js b/apps/gui/ui/i18n-en.js new file mode 100644 index 0000000..fb90797 --- /dev/null +++ b/apps/gui/ui/i18n-en.js @@ -0,0 +1,98 @@ +// Dicionário português -> inglês da interface. {n} marca um valor variável. +export const EN = { + // index.html / sidebar + 'auditoria de Wi-Fi': 'Wi-Fi auditing', + 'Interfaces': 'Interfaces', + 'nenhuma interface Wi-Fi encontrada': 'no Wi-Fi interface found', + 'checar atualização': 'check for update', + 'checando…': 'checking…', + 'atualizar para {n}': 'update to {n}', + 'já atualizado': 'already up to date', + + // topbar + 'Painel de captura': 'Capture dashboard', + 'selecione uma interface': 'select an interface', + 'modo monitor': 'monitor mode', + 'modo gerenciado': 'managed mode', + + // elevação / avisos + 'Pedindo privilégio de administrador — necessário pra modo monitor e captura…': + 'Requesting administrator privilege — needed for monitor mode and capture…', + 'tentar de novo': 'try again', + 'Não foi possível obter privilégio de administrador.': 'Could not get administrator privilege.', + 'Use somente em redes próprias ou com autorização explícita do responsável. Toda ativação de modo monitor e sessão de captura fica registrada na trilha de auditoria local.': + 'Use only on your own networks or with the explicit authorization of the responsible party. Every monitor mode activation and capture session is logged in the local audit trail.', + + // controles + 'ativar modo monitor': 'enable monitor mode', + 'desativar modo monitor': 'disable monitor mode', + 'iniciar captura': 'start capture', + 'parar captura': 'stop capture', + + // resultados + 'Redes vistas': 'Networks seen', + 'Pronto pra capturar': 'Ready to capture', + 'Ative o modo monitor e inicie a captura pra ver as redes no ar.': 'Enable monitor mode and start capturing to see the networks on the air.', + 'Nenhuma interface Wi-Fi': 'No Wi-Fi interface', + 'Conecte um adaptador compatível com modo monitor e recarregue.': 'Connect an adapter that supports monitor mode and reload.', + 'Escutando o ar…': 'Listening to the air…', + 'Nenhuma rede vista ainda. Isso pode levar alguns segundos enquanto os canais são varridos.': + 'No network seen yet. This can take a few seconds while the channels are scanned.', + 'oculta': 'hidden', + 'Rede': 'Network', + 'Canal': 'Channel', + 'Sinal': 'Signal', + 'Segurança': 'Security', + 'Clientes': 'Clients', + + // erros de UI + 'Não foi possível trocar o modo:': 'Could not change the mode:', + 'erro desconhecido': 'unknown error', + 'rota desconhecida': 'unknown route', + + // seletor de idioma + 'Idioma': 'Language', + 'Auto': 'Auto', + 'Português': 'Portuguese', + 'English': 'English', + + // crates/sabia-core/src/error.rs + 'interface `{n}` não encontrada': 'interface `{n}` not found', + 'interface `{n}` não é uma interface Wi-Fi (sem phy80211)': 'interface `{n}` is not a Wi-Fi interface (no phy80211)', + 'falha ao executar `{n}`: {n}': 'failed to run `{n}`: {n}', + 'permissão insuficiente — operações de captura e modo monitor exigem root (CAP_NET_ADMIN/CAP_NET_RAW)': + 'insufficient permission — capture and monitor mode operations require root (CAP_NET_ADMIN/CAP_NET_RAW)', + 'captura crua só é suportada no Linux (AF_PACKET)': 'raw capture is only supported on Linux (AF_PACKET)', + 'entrada inválida: {n}': 'invalid input: {n}', + 'captura .pcap inválida: {n}': 'invalid .pcap capture: {n}', + 'erro de E/S: {n}': 'I/O error: {n}', + 'erro ao (de)serializar: {n}': '(de)serialization error: {n}', + + // crates/sabia-core/src/doctor.rs + 'sem root: modo monitor, captura e injeção vão falhar (use sudo ou o app desktop)': + 'no root: monitor mode, capture and injection will fail (use sudo or the desktop app)', + '`iw` não encontrado no PATH — instale iw/iproute2': '`iw` not found in PATH — install iw/iproute2', + '`ip` não encontrado no PATH — instale iproute2': '`ip` not found in PATH — install iproute2', + 'nenhuma interface anuncia suporte a modo monitor': 'no interface advertises monitor mode support', + '`curl` não encontrado — a auto-atualização não funciona': '`curl` not found — auto-update does not work', + 'a trilha de auditoria existente está corrompida': 'the existing audit trail is corrupted', + 'trilha de auditoria ainda não criada em {n}': 'audit trail not created yet at {n}', + 'cadeia de auditoria rompida na entrada {n}': 'audit chain broken at entry {n}', + 'não foi possível ler a trilha de auditoria: {n}': 'could not read the audit trail: {n}', + 'indisponível nesta plataforma': 'unavailable on this platform', + + // crates/sabia-core/src/engine.rs + '{n} está em modo managed; ative o modo monitor antes de capturar': + '{n} is in managed mode; enable monitor mode before capturing', + '{n} está em modo managed; ative o modo monitor antes de injetar': + '{n} is in managed mode; enable monitor mode before injecting', + 'nenhuma sessão de captura em {n}': 'no capture session on {n}', + 'link-type não é 802.11 nem radiotap': 'link-type is neither 802.11 nor radiotap', + 'handshake não encontrado': 'handshake not found', + 'handshake sem quadros crus (captura antiga)': 'handshake with no raw frames (old capture)', + 'nenhum handshake com quadros crus para exportar': 'no handshake with raw frames to export', + 'dicionário não encontrado: {n}': 'wordlist not found: {n}', + 'nenhum handshake quebrável para os filtros informados': 'no crackable handshake for the given filters', + 'quantidade de quadros deve ficar entre 1 e 256': 'frame count must be between 1 and 256', + 'MAC inválido: {n}': 'invalid MAC: {n}', +}; diff --git a/apps/gui/ui/i18n.js b/apps/gui/ui/i18n.js new file mode 100644 index 0000000..67270b7 --- /dev/null +++ b/apps/gui/ui/i18n.js @@ -0,0 +1,122 @@ +// Idioma da interface. O texto-fonte é em português; no modo inglês um tradutor +// troca os textos exibidos (nós de texto e atributos) usando um dicionário, e +// continua observando a página para traduzir o que for desenhado depois. +// Textos com valores variáveis usam {n} no dicionário (ex.: "{n} redes" -> "{n} networks"). + +import { EN } from './i18n-en.js'; + +let lang = 'pt'; +export const isEn = () => lang === 'en'; +export const currentLang = () => lang; +export const LOC = () => (lang === 'en' ? 'en-US' : 'pt-BR'); + +export function resolveLang(pref) { + if (pref === 'pt' || pref === 'en') return pref; + return (navigator.language || 'pt').toLowerCase().startsWith('pt') ? 'pt' : 'en'; +} + +const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty']; +// Dados do usuário (SSID, BSSID, MAC, valores mono) nunca são traduzidos. +const SKIP = '.ssid, .bssid, .mono, pre, code, input, textarea, script, style'; + +const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}'))); +const patterns = Object.entries(EN) + .filter(([k]) => k.includes('{n}')) + .map(([k, v]) => { + const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v }; + }) + // padrões mais específicos (mais texto fixo) primeiro + .sort((a, b) => b.re.source.length - a.re.source.length); + +const cache = new Map(); +export function tr(text) { + if (!isEn()) return text; + const lead = text.match(/^\s*/)[0]; + const trail = text.match(/\s*$/)[0]; + const core = text.trim(); + if (!core) return text; + if (cache.has(core)) return lead + cache.get(core) + trail; + let out = exact.get(core); + if (out === undefined) { + for (const p of patterns) { + const m = core.match(p.re); + if (m) { + let i = 0; + // os valores capturados também podem ter partes traduzíveis (ex.: estados) + out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? '')); + break; + } + } + } + if (out === undefined) out = core; + if (cache.size > 5000) cache.clear(); + cache.set(core, out); + return lead + out + trail; +} + +const orig = new WeakMap(); // nó -> texto original em português +let observer = null; + +function translateNode(n) { + if (n.nodeType === 3) { + if (n.parentElement?.closest(SKIP)) return; + const o = orig.get(n) ?? n.nodeValue; + const t = tr(o); + if (t !== n.nodeValue) { + if (!orig.has(n)) orig.set(n, o); + n.nodeValue = t; + } + } else if (n.nodeType === 1) { + if (n.matches?.(SKIP) && n.tagName !== 'INPUT') return; + for (const a of ATTRS) { + if (n.hasAttribute?.(a)) { + const key = `i18n-${a}`; + const o = n[key] ?? n.getAttribute(a); + const t = tr(o); + if (t !== n.getAttribute(a)) { n[key] = o; n.setAttribute(a, t); } + } + } + for (const c of n.childNodes) translateNode(c); + } +} + +function restoreAll(root) { + const w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); + let n; + while ((n = w.nextNode())) { if (orig.has(n)) { n.nodeValue = orig.get(n); orig.delete(n); } } + root.querySelectorAll('*').forEach((el) => { + for (const a of ATTRS) { const key = `i18n-${a}`; if (el[key] !== undefined) { el.setAttribute(a, el[key]); delete el[key]; } } + }); +} + +function start() { + translateNode(document.body); + document.title = tr(document.title); + if (observer) return; + observer = new MutationObserver((muts) => { + observer.disconnect(); + for (const m of muts) { + if (m.type === 'childList') m.addedNodes.forEach(translateNode); + else if (m.type === 'characterData') { orig.delete(m.target); translateNode(m.target); } + else if (m.type === 'attributes') { delete m.target[`i18n-${m.attributeName}`]; translateNode(m.target); } + } + observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ATTRS }); + }); + observer.observe(document.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ATTRS }); +} + +function stop() { + observer?.disconnect(); + observer = null; + restoreAll(document.body); +} + +export function setLang(pref) { + const next = resolveLang(pref); + const changed = next !== lang; + lang = next; + document.documentElement.lang = lang === 'en' ? 'en' : 'pt-BR'; + if (lang === 'en') start(); + else if (changed || observer) stop(); +} diff --git a/apps/gui/ui/index.html b/apps/gui/ui/index.html index e1efa5b..5f5e801 100644 --- a/apps/gui/ui/index.html +++ b/apps/gui/ui/index.html @@ -4,14 +4,14 @@ Sabiá - +
@@ -71,6 +76,6 @@
- + diff --git a/apps/gui/ui/sabia-logo.svg b/apps/gui/ui/sabia-logo.svg deleted file mode 100644 index fabdb8a..0000000 --- a/apps/gui/ui/sabia-logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/gui/ui/sabia.png b/apps/gui/ui/sabia.png new file mode 100644 index 0000000..99c6d12 Binary files /dev/null and b/apps/gui/ui/sabia.png differ diff --git a/crates/sabia-cli/Cargo.toml b/crates/sabia-cli/Cargo.toml index 4fc0c2d..e2c9779 100644 --- a/crates/sabia-cli/Cargo.toml +++ b/crates/sabia-cli/Cargo.toml @@ -15,5 +15,6 @@ path = "src/main.rs" [dependencies] sabia-core = { path = "../sabia-core" } clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } serde_json = "1" ctrlc = "3" diff --git a/crates/sabia-cli/src/main.rs b/crates/sabia-cli/src/main.rs index 600b17d..bcf7152 100644 --- a/crates/sabia-cli/src/main.rs +++ b/crates/sabia-cli/src/main.rs @@ -1,9 +1,9 @@ mod web; use clap::{Parser, Subcommand}; -use sabia_core::{ - audit::AuditLog, capture::RawCapture, iface, netlink, pcapwriter::PcapWriter, scan::ScanTable, -}; +use sabia_core::engine::{Engine, ScanOptions}; +use sabia_core::iface; +use std::io::IsTerminal; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -16,47 +16,115 @@ use std::time::Duration; version )] struct Cli { + /// Trilha de auditoria (padrão: .sabia-audit.log). + #[arg(long, global = true, default_value = ".sabia-audit.log")] + audit: PathBuf, + #[command(subcommand)] cmd: Cmd, } #[derive(Subcommand)] enum Cmd { - /// Lista interfaces de rede e mostra quais são Wi-Fi. + /// Lista interfaces, canais e controla o modo monitor. Iface { #[command(subcommand)] action: IfaceAction, }, - /// Varre o ar na interface (já em modo monitor) e mostra APs/clientes vistos. + /// Captura o ar e mostra redes, clientes e handshakes. Scan { iface: String, - /// Canal fixo; sem isso, pula entre todos os canais suportados. + /// Canal fixo; sem isso, percorre os canais suportados. #[arg(long)] channel: Option, + /// Banda ao percorrer canais: 2.4, 5, 6 ou all. + #[arg(long)] + band: Option, + /// Tempo em cada canal (ms). + #[arg(long, default_value_t = 350)] + hop_ms: u64, /// Para a varredura depois de N segundos (padrão: até Ctrl+C). #[arg(long)] seconds: Option, - /// Salva tudo que foi capturado em um .pcap (inclui handshakes). + /// Salva os quadros capturados em um .pcap. #[arg(long)] out: Option, + /// Depois de capturar, tenta quebrar o primeiro handshake. + #[arg(long, value_name = "DICIONARIO")] + crack: Option, + /// Mostra apenas o JSON final. + #[arg(long)] + json: bool, + }, + /// Analisa um .pcap offline (redes, clientes e handshakes). + Analyze { + pcap: PathBuf, + #[arg(long)] + json: bool, + }, + /// Quebra um handshake de um .pcap por dicionário. + Crack { + pcap: PathBuf, + /// Dicionário com uma senha por linha. + #[arg(short, long)] + wordlist: PathBuf, + #[arg(long)] + bssid: Option, + #[arg(long)] + station: Option, + #[arg(long)] + json: bool, + }, + /// Envia desautenticação para um AP/estação (teste autorizado). + Deauth { + iface: String, + #[arg(long)] + bssid: String, + /// Sem isso, o destino é broadcast. + #[arg(long)] + station: Option, + #[arg(long, default_value_t = 5)] + count: u32, + #[arg(long, default_value_t = 7)] + reason: u16, + /// Confirma que você tem autorização para testar esta rede. + #[arg(long)] + yes: bool, + }, + /// Diagnóstico de privilégios, ferramentas e interfaces. + Doctor { + #[arg(long)] + json: bool, + }, + /// Inspeciona a trilha de auditoria. + Audit { + #[command(subcommand)] + action: AuditAction, }, /// Verifica e aplica atualizações do próprio sabia. Update { #[arg(long)] apply: bool, }, - /// Confere a integridade da trilha de auditoria. - AuditVerify { path: PathBuf }, /// Abre a interface gráfica (servidor local + navegador). Ui { #[arg(long, default_value = "4337")] port: u16, + /// Não tenta abrir o navegador automaticamente. + #[arg(long)] + no_open: bool, }, } #[derive(Subcommand)] enum IfaceAction { List, + /// Mostra os canais suportados pelo rádio. + Channels { + iface: String, + #[arg(long)] + json: bool, + }, /// Liga/desliga modo monitor numa interface Wi-Fi. Monitor { iface: String, @@ -65,16 +133,23 @@ enum IfaceAction { }, } +#[derive(Subcommand)] +enum AuditAction { + /// Confere a integridade da cadeia. + Verify { path: Option }, + /// Mostra as últimas entradas. + Show { + #[arg(long, default_value_t = 20)] + limit: usize, + }, +} + #[derive(Clone, Copy, clap::ValueEnum)] enum OnOff { On, Off, } -fn audit_path() -> PathBuf { - PathBuf::from(".sabia-audit.log") -} - fn main() { let cli = Cli::parse(); if let Err(e) = run(cli) { @@ -84,6 +159,9 @@ fn main() { } fn run(cli: Cli) -> sabia_core::Result<()> { + let audit_path = cli.audit.clone(); + let engine = Engine::with_audit_path(&audit_path); + match cli.cmd { Cmd::Iface { action } => match action { IfaceAction::List => { @@ -94,17 +172,33 @@ fn run(cli: Cli) -> sabia_core::Result<()> { i.name, i.mac, tag, - format!("{:?}", i.mode), + format!("{:?}", i.mode).to_lowercase(), if i.up { "up" } else { "down" } ); } } + IfaceAction::Channels { iface: name, json } => { + let channels = engine.channels(&name)?; + if json { + println!("{}", serde_json::to_string_pretty(&channels)?); + } else { + for c in channels { + println!( + "canal {:<4} {:>5} MHz {:<6} {}{}", + c.channel, + c.freq_mhz, + c.band.label(), + if c.disabled { "desabilitado" } else { "ok" }, + c.max_power_dbm + .map(|p| format!(" até {p:.1} dBm")) + .unwrap_or_default() + ); + } + } + } IfaceAction::Monitor { iface: name, state } => { let enable = matches!(state, OnOff::On); - iface::get(&name)?; // valida que é Wi-Fi antes de tocar nela - netlink::set_monitor_mode(&name, enable)?; - let mut log = AuditLog::open(audit_path())?; - log.record("monitor_mode", &name, &format!("{{\"enable\": {enable}}}"))?; + engine.set_monitor(&name, enable)?; println!( "{name}: modo monitor {}", if enable { "ativado" } else { "desativado" } @@ -115,104 +209,167 @@ fn run(cli: Cli) -> sabia_core::Result<()> { Cmd::Scan { iface: name, channel, + band, + hop_ms, seconds, out, + crack, + json, } => { - let info = iface::get(&name)?; - if info.mode != iface::Mode::Monitor { - eprintln!("aviso: {name} não está em modo monitor (sabia iface monitor {name} on)"); - } - - let mut log = AuditLog::open(audit_path())?; - log.record( - "scan_start", - &name, - &format!("{{\"channel\": {channel:?}}}"), - )?; - - let cap = RawCapture::open(&name)?; - cap.set_read_timeout(Duration::from_millis(500))?; + let options = ScanOptions { + channel, + band, + hop_ms, + pcap_out: out, + duration_secs: seconds, + }; + engine.start_scan_with(&name, options)?; let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); - ctrlc::set_handler(move || r.store(false, Ordering::SeqCst)) - .expect("falha ao instalar handler de Ctrl+C"); + let _ = ctrlc::set_handler(move || r.store(false, Ordering::SeqCst)); - let mut table = ScanTable::new(); - let mut writer = match &out { - Some(p) => Some(PcapWriter::create(p)?), - None => None, - }; - - let channels: Vec = match channel { - Some(c) => vec![c], - None => netlink::supported_channels(&name).unwrap_or_default(), - }; - let mut chan_idx = 0usize; - let mut last_hop = std::time::Instant::now(); - - let deadline = seconds.map(|s| std::time::Instant::now() + Duration::from_secs(s)); - let mut buf = [0u8; 65535]; - - println!("capturando em {name}... (Ctrl+C para parar)"); + let interactive = std::io::stdout().is_terminal() && !json; while running.load(Ordering::SeqCst) { - if let Some(d) = deadline { - if std::time::Instant::now() >= d { - break; - } + std::thread::sleep(Duration::from_secs(1)); + let snapshot = match engine.scan_snapshot(&name) { + Ok(s) => s, + Err(_) => break, + }; + if !snapshot.running { + break; } - if channel.is_none() - && !channels.is_empty() - && last_hop.elapsed() > Duration::from_millis(300) - { - chan_idx = (chan_idx + 1) % channels.len(); - let _ = netlink::set_channel(&name, channels[chan_idx]); - last_hop = std::time::Instant::now(); - } - - match cap.recv(&mut buf)? { - Some(n) => { - let frame = &buf[..n]; - let had_eapol = table.ingest(frame); - if had_eapol { - eprintln!("[!] handshake EAPOL visto"); - } - if let Some(w) = writer.as_mut() { - w.write_frame(frame)?; - } - } - None => continue, // timeout, sem quadro + if interactive { + print!("\x1b[2J\x1b[H"); + print_snapshot(&snapshot); } } - if let Some(w) = writer.as_mut() { - w.flush()?; + + engine.stop_scan(&name); + let snapshot = engine.scan_snapshot(&name)?; + if json { + println!("{}", serde_json::to_string_pretty(&snapshot)?); + } else { + if interactive { + print!("\x1b[2J\x1b[H"); + } + print_snapshot(&snapshot); } - log.record( - "scan_stop", - &name, - &format!("{{\"aps_found\": {}}}", table.access_points().len()), + if let Some(wordlist) = crack { + crack_first(&engine, &name, &wordlist)?; + } + } + + Cmd::Analyze { pcap, json } => { + let snapshot = engine.analyze_pcap(&pcap)?; + if json { + println!("{}", serde_json::to_string_pretty(&snapshot)?); + } else { + print_snapshot(&snapshot); + } + } + + Cmd::Crack { + pcap, + wordlist, + bssid, + station, + json, + } => { + let mut last = 0u64; + let report = sabia_core::crack::crack_pcap( + &pcap, + &wordlist, + bssid.as_deref(), + station.as_deref(), + |tried| { + if tried.saturating_sub(last) >= 4096 { + eprint!("\r{tried} senhas testadas…"); + last = tried; + } + }, )?; - - println!( - "\n{:<18} {:<24} {:<8} {:<8} {:<6} CLIENTES", - "BSSID", "SSID", "CANAL", "SINAL", "SEG" - ); - for ap in table.access_points() { + if !json { + eprintln!(); + } + if json { + println!("{}", serde_json::to_string_pretty(&report)?); + } else if let Some(password) = &report.password { println!( - "{:<18} {:<24} {:<8} {:<8} {:<6} {}", - ap.bssid, - ap.ssid.unwrap_or_default(), - ap.channel.map(|c| c.to_string()).unwrap_or_default(), - ap.last_signal_dbm - .map(|s| format!("{s}dBm")) - .unwrap_or_default(), - ap.security.unwrap_or_default(), - ap.clients.len() + "SENHA ENCONTRADA: {password}\n{} tentativas em {} ms ({})", + report.tried, + report.elapsed_ms, + report.method.as_deref().unwrap_or("?") + ); + } else { + println!( + "senha não encontrada ({} tentativas em {} ms)", + report.tried, report.elapsed_ms ); } } + Cmd::Deauth { + iface: name, + bssid, + station, + count, + reason, + yes, + } => { + if !yes { + return Err(sabia_core::SabiaError::InvalidInput( + "desautenticação derruba clientes; confirme com --yes apenas em rede própria ou com autorização".into(), + )); + } + let report = engine.deauth(&name, &bssid, station.as_deref(), count, reason)?; + println!( + "{} quadros enviados para {} via {} (motivo {})", + report.frames_sent, report.station, report.iface, report.reason + ); + } + + Cmd::Doctor { json } => { + let report = engine.doctor(); + if json { + println!("{}", serde_json::to_string_pretty(&report)?); + } else { + print_doctor(&report); + } + } + + Cmd::Audit { action } => match action { + AuditAction::Verify { path } => { + let path = path.unwrap_or_else(|| audit_path.clone()); + if !path.exists() { + println!( + "{} não existe (nenhuma ação registrada ainda)", + path.display() + ); + } else { + match sabia_core::audit::AuditLog::verify(&path)? { + None => println!("cadeia de auditoria íntegra ({})", path.display()), + Some(idx) => { + println!("cadeia ROMPIDA na entrada {idx} ({})", path.display()) + } + } + } + } + AuditAction::Show { limit } => { + let entries = sabia_core::audit::AuditLog::read_entries(&audit_path, Some(limit))?; + if entries.is_empty() { + println!("nenhuma entrada em {}", audit_path.display()); + } + for e in entries { + println!( + "{} {:<18} {:<18} {}", + e.timestamp, e.action, e.target, e.detail + ); + } + } + }, + Cmd::Update { apply } => { let current = env!("CARGO_PKG_VERSION"); if apply { @@ -233,14 +390,148 @@ fn run(cli: Cli) -> sabia_core::Result<()> { } } - Cmd::AuditVerify { path } => match sabia_core::audit::AuditLog::verify(&path)? { - None => println!("cadeia de auditoria íntegra"), - Some(idx) => println!("cadeia ROMPIDA na entrada {idx}"), - }, - - Cmd::Ui { port } => { - web::serve(port)?; + Cmd::Ui { port, no_open } => { + web::serve(port, audit_path, !no_open)?; } } Ok(()) } + +fn crack_first(engine: &Engine, iface: &str, wordlist: &PathBuf) -> sabia_core::Result<()> { + let Some(hs) = engine + .handshakes(Some(iface)) + .into_iter() + .find(|h| h.crackable) + else { + println!("\nnenhum handshake quebrável capturado"); + return Ok(()); + }; + println!( + "\nquebrando handshake de {} ({}) com {}…", + hs.ssid.as_deref().unwrap_or("?"), + hs.bssid, + wordlist.display() + ); + let id = engine.start_crack(iface, Some(&hs.bssid), Some(&hs.station), wordlist)?; + loop { + let job = engine.crack_job(&id).expect("job"); + if job.status == "running" { + eprint!("\r{} senhas testadas…", job.tried); + std::thread::sleep(Duration::from_millis(500)); + continue; + } + eprintln!(); + match job.report { + Some(report) => match report.password { + Some(password) => println!( + "SENHA ENCONTRADA: {password}\n{} tentativas em {} ms", + report.tried, report.elapsed_ms + ), + None => println!( + "senha não encontrada ({} tentativas em {} ms)", + report.tried, report.elapsed_ms + ), + }, + None => println!("falha: {}", job.error.unwrap_or_default()), + } + break; + } + Ok(()) +} + +fn print_snapshot(snapshot: &sabia_core::scan::ScanSnapshot) { + println!( + "{} {}{} {} quadros · {} APs · {} clientes · {} handshakes", + snapshot.iface, + if snapshot.running { "● " } else { "" }, + snapshot + .channel + .map(|c| format!("canal {c}")) + .unwrap_or_default(), + snapshot.stats.frames, + snapshot.stats.aps, + snapshot.stats.clients, + snapshot.stats.handshakes + ); + if !snapshot.access_points.is_empty() { + println!( + "\n{:<18} {:<24} {:<6} {:<8} {:<12} CLIENTES", + "BSSID", "SSID", "CANAL", "SINAL", "SEGURANÇA" + ); + for ap in &snapshot.access_points { + println!( + "{:<18} {:<24} {:<6} {:<8} {:<12} {}", + ap.bssid, + ap.ssid.clone().unwrap_or_else(|| "".into()), + ap.channel.map(|c| c.to_string()).unwrap_or_default(), + ap.signal_dbm + .map(|s| format!("{s} dBm")) + .unwrap_or_default(), + ap.security.clone().unwrap_or_default(), + ap.clients.len() + ); + } + } + if !snapshot.handshakes.is_empty() { + println!("\nHANDSHAKES"); + for hs in &snapshot.handshakes { + println!( + "{:<18} {:<18} {:<16} msgs {:<8} {}{}", + hs.bssid, + hs.station, + hs.ssid.clone().unwrap_or_default(), + hs.messages, + hs.kind, + if hs.crackable { " · quebrável" } else { "" } + ); + } + } +} + +fn print_doctor(report: &sabia_core::doctor::DoctorReport) { + println!("Sabiá {} ({})", report.version, report.platform); + println!( + "root: {} · iw: {} · ip: {} · curl: {} · captura: {}", + yes_no(report.root), + yes_no(report.iw), + yes_no(report.ip), + yes_no(report.curl), + report.capture_backend + ); + println!( + "auditoria: {} ({})", + report.audit_path, + if report.audit_ok { + "íntegra" + } else { + "corrompida" + } + ); + if !report.interfaces.is_empty() { + println!("\nINTERFACES"); + for i in &report.interfaces { + println!( + "{:<16} {:<18} {:<10} monitor: {:<3} driver: {}", + i.name, + i.mac, + i.mode, + yes_no(i.monitor_supported), + i.driver.as_deref().unwrap_or("?") + ); + } + } + if !report.notes.is_empty() { + println!("\nAVISOS"); + for note in &report.notes { + println!("- {note}"); + } + } +} + +fn yes_no(value: bool) -> &'static str { + if value { + "sim" + } else { + "não" + } +} diff --git a/crates/sabia-cli/src/web/mod.rs b/crates/sabia-cli/src/web/mod.rs index e042302..26e762f 100644 --- a/crates/sabia-cli/src/web/mod.rs +++ b/crates/sabia-cli/src/web/mod.rs @@ -3,31 +3,32 @@ //! núcleo por uma API JSON simples. A mesma página em `apps/gui/ui` é o //! que o app desktop (Tauri) carrega — só muda o transporte. -use sabia_core::engine::Engine; +use sabia_core::engine::{Engine, ScanOptions}; +use serde::Deserialize; use std::collections::HashMap; -use std::io::{BufRead, BufReader, Write}; +use std::io::{BufRead, BufReader, Read, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::Arc; use std::thread; +use std::time::{SystemTime, UNIX_EPOCH}; const INDEX_HTML: &str = include_str!("../../../../apps/gui/ui/index.html"); const APP_CSS: &str = include_str!("../../../../apps/gui/ui/app.css"); const APP_JS: &str = include_str!("../../../../apps/gui/ui/app.js"); -const LOGO_SVG: &str = include_str!("../../../../apps/gui/ui/sabia-logo.svg"); +const LOGO_PNG: &[u8] = include_bytes!("../../../../apps/gui/ui/sabia.png"); -pub fn serve(port: u16) -> sabia_core::Result<()> { +pub fn serve(port: u16, audit_path: std::path::PathBuf, open: bool) -> sabia_core::Result<()> { let token = gen_token(); let listener = TcpListener::bind(("127.0.0.1", port))?; - let engine = Arc::new(Engine::new()); + let engine = Arc::new(Engine::with_audit_path(audit_path)); println!("Sabiá UI em http://127.0.0.1:{port}/?token={token}"); - let _ = open_browser(&format!("http://127.0.0.1:{port}/?token={token}")); + if open { + let _ = open_browser(&format!("http://127.0.0.1:{port}/?token={token}")); + } for stream in listener.incoming() { - let stream = match stream { - Ok(s) => s, - Err(_) => continue, - }; + let Ok(stream) = stream else { continue }; let engine = engine.clone(); let token = token.clone(); thread::spawn(move || { @@ -38,36 +39,35 @@ pub fn serve(port: u16) -> sabia_core::Result<()> { } fn gen_token() -> String { - use std::time::{SystemTime, UNIX_EPOCH}; let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_nanos(); format!("{nanos:x}") } +#[cfg(target_os = "linux")] fn open_browser(url: &str) -> std::io::Result<()> { - #[cfg(target_os = "linux")] - { - std::process::Command::new("xdg-open") - .arg(url) - .spawn() - .map(|_| ()) - } - #[cfg(target_os = "macos")] - { - std::process::Command::new("open") - .arg(url) - .spawn() - .map(|_| ()) - } - #[cfg(windows)] - { - std::process::Command::new("cmd") - .args(["/C", "start", "", url]) - .spawn() - .map(|_| ()) - } + std::process::Command::new("xdg-open") + .arg(url) + .spawn() + .map(|_| ()) +} + +#[cfg(target_os = "macos")] +fn open_browser(url: &str) -> std::io::Result<()> { + std::process::Command::new("open") + .arg(url) + .spawn() + .map(|_| ()) +} + +#[cfg(windows)] +fn open_browser(url: &str) -> std::io::Result<()> { + std::process::Command::new("cmd") + .args(["/C", "start", "", url]) + .spawn() + .map(|_| ()) } struct Request { @@ -75,6 +75,44 @@ struct Request { path: String, query: HashMap, token: Option, + body: Vec, +} + +impl Request { + fn text(&self, key: &str) -> Option { + if !self.body.is_empty() { + if let Ok(serde_json::Value::Object(map)) = + serde_json::from_slice::(&self.body) + { + if let Some(value) = map.get(key) { + return match value { + serde_json::Value::String(s) => Some(s.clone()), + serde_json::Value::Null => None, + other => Some(other.to_string()), + }; + } + } + } + self.query.get(key).cloned() + } + + fn bool(&self, key: &str) -> bool { + self.text(key) + .map(|v| v == "true" || v == "1" || v == "on") + .unwrap_or(false) + } + + fn u64(&self, key: &str) -> Option { + self.text(key).and_then(|v| v.parse().ok()) + } + + fn u32(&self, key: &str) -> Option { + self.text(key).and_then(|v| v.parse().ok()) + } + + fn u16(&self, key: &str) -> Option { + self.text(key).and_then(|v| v.parse().ok()) + } } fn parse_request(stream: &mut TcpStream) -> std::io::Result { @@ -85,37 +123,87 @@ fn parse_request(stream: &mut TcpStream) -> std::io::Result { let method = parts.next().unwrap_or("GET").to_string(); let full_path = parts.next().unwrap_or("/").to_string(); + let mut content_length = 0usize; + let mut header_token = None; loop { let mut l = String::new(); - if reader.read_line(&mut l)? == 0 || l == "\r\n" { + if reader.read_line(&mut l)? == 0 || l == "\r\n" || l == "\n" { break; } + if let Some((name, value)) = l.split_once(':') { + let name = name.trim().to_ascii_lowercase(); + let value = value.trim(); + match name.as_str() { + "content-length" => content_length = value.parse().unwrap_or(0), + "x-sabia-token" => header_token = Some(value.to_string()), + "authorization" => { + header_token = value + .strip_prefix("Bearer ") + .map(|v| v.trim().to_string()) + .or_else(|| Some(value.to_string())); + } + _ => {} + } + } } - let (path, query_str) = match full_path.split_once('?') { - Some((p, q)) => (p.to_string(), q.to_string()), - None => (full_path, String::new()), - }; + let (path, query_str) = full_path + .split_once('?') + .map(|(p, q)| (p.to_string(), q.to_string())) + .unwrap_or((full_path, String::new())); let mut query = HashMap::new(); for pair in query_str.split('&') { if let Some((k, v)) = pair.split_once('=') { query.insert(urldecode(k), urldecode(v)); + } else if !pair.is_empty() { + query.insert(urldecode(pair), String::new()); } } - let token = query.get("token").cloned(); + let token = query.get("token").cloned().or(header_token); + + let mut body = vec![0u8; content_length.min(4 * 1024 * 1024)]; + if !body.is_empty() { + reader.read_exact(&mut body)?; + } + Ok(Request { method, path, query, token, + body, }) } fn urldecode(s: &str) -> String { - s.replace('+', " ") - .replace("%3A", ":") - .replace("%2F", "/") - .replace("%20", " ") + let bytes = s.as_bytes(); + let mut out = Vec::with_capacity(bytes.len()); + let mut i = 0; + while i < bytes.len() { + match bytes[i] { + b'+' => out.push(b' '), + b'%' if i + 2 < bytes.len() => { + if let (Some(hi), Some(lo)) = (hexval(bytes[i + 1]), hexval(bytes[i + 2])) { + out.push((hi << 4) | lo); + i += 2; + } else { + out.push(b'%'); + } + } + b => out.push(b), + } + i += 1; + } + String::from_utf8_lossy(&out).into_owned() +} + +fn hexval(b: u8) -> Option { + match b { + b'0'..=b'9' => Some(b - b'0'), + b'a'..=b'f' => Some(b - b'a' + 10), + b'A'..=b'F' => Some(b - b'A' + 10), + _ => None, + } } fn handle( @@ -137,7 +225,6 @@ fn handle( ); } - // Assets estáticos: não carregam dado nenhum, então não exigem token. match req.path.as_str() { "/app.css" => return respond(&mut stream, 200, "text/css", APP_CSS.as_bytes()), "/app.js" => { @@ -148,9 +235,7 @@ fn handle( APP_JS.as_bytes(), ) } - "/sabia-logo.svg" => { - return respond(&mut stream, 200, "image/svg+xml", LOGO_SVG.as_bytes()) - } + "/sabia.png" => return respond(&mut stream, 200, "image/png", LOGO_PNG), _ => {} } @@ -162,65 +247,253 @@ fn handle( &mut stream, 401, "application/json", - b"{\"error\":\"token invalido\"}", + b"{\"ok\":false,\"error\":\"token invalido\"}", ); } - let body = api(engine, &req); - respond(&mut stream, 200, "application/json", body.as_bytes()) + let (code, body) = api(engine, &req); + respond(&mut stream, code, "application/json", body.as_bytes()) } fn respond(stream: &mut TcpStream, code: u16, ctype: &str, body: &[u8]) -> std::io::Result<()> { let status = match code { 200 => "OK", + 400 => "Bad Request", 401 => "Unauthorized", 404 => "Not Found", + 500 => "Internal Server Error", _ => "Error", }; - write!(stream, "HTTP/1.1 {code} {status}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len())?; + write!( + stream, + "HTTP/1.1 {code} {status}\r\nContent-Type: {ctype}\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n", + body.len() + )?; stream.write_all(body)?; Ok(()) } -fn api(engine: &Arc, req: &Request) -> String { +fn ok(value: &T) -> (u16, String) { + match serde_json::to_string(value) { + Ok(body) => (200, body), + Err(e) => err(500, e.to_string()), + } +} + +fn ok_json(value: serde_json::Value) -> (u16, String) { + (200, value.to_string()) +} + +fn err(code: u16, message: impl ToString) -> (u16, String) { + ( + code, + serde_json::json!({ "ok": false, "error": message.to_string() }).to_string(), + ) +} + +#[derive(Deserialize)] +struct AnalyzeBody { + pcap: String, +} + +fn api(engine: &Arc, req: &Request) -> (u16, String) { match (req.method.as_str(), req.path.as_str()) { - ("GET", "/api/version") => format!("{{\"version\":{:?}}}", engine.version()), + ("GET", "/api/version") => ok_json(serde_json::json!({ "version": engine.version() })), + ("GET", "/api/doctor") => ok(&engine.doctor()), ("GET", "/api/update/check") => match engine.update_check() { - Ok(status) => serde_json::to_string(&status).unwrap_or_else(|_| "{}".into()), - Err(e) => format!("{{\"error\":{:?}}}", e.to_string()), + Ok(status) => ok(&status), + Err(e) => err(400, e), }, - ("GET", "/api/ifaces") => { - let list = engine.list_ifaces().unwrap_or_default(); - serde_json::to_string(&list).unwrap_or_else(|_| "[]".into()) + ("POST", "/api/update/apply") => match engine.update_apply() { + Ok(version) => ok_json(serde_json::json!({ "ok": true, "version": version })), + Err(e) => err(400, e), + }, + ("GET", "/api/ifaces") => match engine.list_ifaces() { + Ok(list) => ok(&list), + Err(e) => err(400, e), + }, + ("GET", "/api/sessions") => ok(&engine.list_sessions()), + ("GET", "/api/channels") => { + let Some(iface) = req.text("iface") else { + return err(400, "iface obrigatória"); + }; + match engine.channels(&iface) { + Ok(list) => ok(&list), + Err(e) => err(400, e), + } } ("POST", "/api/monitor") => { - let name = req.query.get("iface").cloned().unwrap_or_default(); - let enable = req - .query - .get("enable") - .map(|v| v == "true") - .unwrap_or(false); - match engine.set_monitor(&name, enable) { - Ok(()) => "{\"ok\":true}".into(), - Err(e) => format!("{{\"ok\":false,\"error\":{:?}}}", e.to_string()), + let Some(iface) = req.text("iface") else { + return err(400, "iface obrigatória"); + }; + match engine.set_monitor(&iface, req.bool("enable")) { + Ok(()) => ok_json(serde_json::json!({ "ok": true })), + Err(e) => err(400, e), + } + } + ("POST", "/api/channel") => { + let (Some(iface), Some(channel)) = (req.text("iface"), req.u32("channel")) else { + return err(400, "iface e channel obrigatórios"); + }; + match engine.set_channel(&iface, channel) { + Ok(()) => ok_json(serde_json::json!({ "ok": true })), + Err(e) => err(400, e), } } ("POST", "/api/scan/start") => { - let name = req.query.get("iface").cloned().unwrap_or_default(); - match engine.start_scan(&name) { - Ok(()) => "{\"ok\":true}".into(), - Err(e) => format!("{{\"ok\":false,\"error\":{:?}}}", e.to_string()), + let Some(iface) = req.text("iface") else { + return err(400, "iface obrigatória"); + }; + let options = ScanOptions { + channel: req.u32("channel"), + band: req.text("band"), + hop_ms: req.u64("hop_ms").unwrap_or(350), + pcap_out: req.text("pcap_out").map(Into::into), + duration_secs: req.u64("seconds"), + }; + match engine.start_scan_with(&iface, options) { + Ok(()) => ok_json(serde_json::json!({ "ok": true })), + Err(e) => err(400, e), } } ("POST", "/api/scan/stop") => { - let name = req.query.get("iface").cloned().unwrap_or_default(); - engine.stop_scan(&name); - "{\"ok\":true}".into() + let Some(iface) = req.text("iface") else { + return err(400, "iface obrigatória"); + }; + engine.stop_scan(&iface); + ok_json(serde_json::json!({ "ok": true })) + } + ("GET", "/api/scan/snapshot") => { + let Some(iface) = req.text("iface") else { + return err(400, "iface obrigatória"); + }; + match engine.scan_snapshot(&iface) { + Ok(snapshot) => ok(&snapshot), + Err(e) => err(400, e), + } } ("GET", "/api/scan") => { - let name = req.query.get("iface").cloned().unwrap_or_default(); - serde_json::to_string(&engine.scan_results(&name)).unwrap_or_else(|_| "[]".into()) + let Some(iface) = req.text("iface") else { + return err(400, "iface obrigatória"); + }; + ok(&engine.scan_results(&iface)) } - _ => "{\"error\":\"rota desconhecida\"}".into(), + ("GET", "/api/scan/clients") => { + let Some(iface) = req.text("iface") else { + return err(400, "iface obrigatória"); + }; + ok(&engine.scan_clients(&iface)) + } + ("GET", "/api/handshakes") => ok(&engine.handshakes(req.text("iface").as_deref())), + ("POST", "/api/handshakes/export") => { + let (Some(iface), Some(bssid), Some(station), Some(out)) = ( + req.text("iface"), + req.text("bssid"), + req.text("station"), + req.text("out"), + ) else { + return err(400, "iface, bssid, station e out são obrigatórios"); + }; + match engine.export_handshake(&iface, &bssid, &station, &out) { + Ok(report) => ok(&report), + Err(e) => err(400, e), + } + } + ("POST", "/api/handshakes/export-all") => { + let (Some(iface), Some(out)) = (req.text("iface"), req.text("out")) else { + return err(400, "iface e out são obrigatórios"); + }; + match engine.export_all_handshakes(&iface, &out) { + Ok(report) => ok(&report), + Err(e) => err(400, e), + } + } + ("POST", "/api/crack") => { + let (Some(iface), Some(wordlist)) = (req.text("iface"), req.text("wordlist")) else { + return err(400, "iface e wordlist são obrigatórios"); + }; + let bssid = req.text("bssid"); + let station = req.text("station"); + match engine.start_crack(&iface, bssid.as_deref(), station.as_deref(), &wordlist) { + Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })), + Err(e) => err(400, e), + } + } + ("GET", "/api/crack") => { + let Some(id) = req.text("id") else { + return err(400, "id obrigatório"); + }; + match engine.crack_job(&id) { + Some(job) => ok(&job), + None => err(404, "job não encontrado"), + } + } + ("GET", "/api/cracks") => ok(&engine.crack_jobs()), + ("POST", "/api/deauth") => { + if !req.bool("confirm") { + return err( + 400, + "confirme a autorização do teste (confirm=true) antes de desautenticar", + ); + } + let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else { + return err(400, "iface e bssid são obrigatórios"); + }; + let count = req.u32("count").unwrap_or(5); + let reason = req.u16("reason").unwrap_or(7); + match engine.deauth( + &iface, + &bssid, + req.text("station").as_deref(), + count, + reason, + ) { + Ok(report) => ok(&report), + Err(e) => err(400, e), + } + } + ("POST", "/api/analyze") => { + let pcap = if !req.body.is_empty() { + serde_json::from_slice::(&req.body) + .ok() + .map(|b| b.pcap) + .or_else(|| req.text("pcap")) + } else { + req.text("pcap") + }; + let Some(pcap) = pcap else { + return err(400, "pcap obrigatório"); + }; + match engine.analyze_pcap(&pcap) { + Ok(snapshot) => ok(&snapshot), + Err(e) => err(400, e), + } + } + ("GET", "/api/audit/verify") => match engine.audit_verify() { + Ok(None) => ok_json(serde_json::json!({ "ok": true, "broken_at": null })), + Ok(Some(idx)) => ok_json(serde_json::json!({ "ok": false, "broken_at": idx })), + Err(e) => err(400, e), + }, + ("GET", "/api/audit") => { + let limit = req.u64("limit").map(|v| v as usize).or(Some(50)); + match engine.audit_entries(limit) { + Ok(entries) => ok(&entries), + Err(e) => err(400, e), + } + } + _ => err(404, "rota desconhecida"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn decodes_url_components() { + assert_eq!(urldecode("a%20b%2Fc+d"), "a b/c d"); + assert_eq!(urldecode("100%25"), "100%"); + assert_eq!(urldecode("caf%C3%A9"), "café"); + assert_eq!(urldecode("bad%2"), "bad%2"); } } diff --git a/crates/sabia-core/Cargo.toml b/crates/sabia-core/Cargo.toml index 72b642c..3f6e747 100644 --- a/crates/sabia-core/Cargo.toml +++ b/crates/sabia-core/Cargo.toml @@ -15,6 +15,12 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" time = { version = "0.3", features = ["formatting", "macros"] } sha2 = "0.10" +sha1 = "0.10" +hmac = "0.12" +md-5 = "0.10" + +[dev-dependencies] +tempfile = "3" [target.'cfg(not(target_os = "linux"))'.dependencies] # captura crua (AF_PACKET) só existe no Linux; outras plataformas compilam diff --git a/crates/sabia-core/src/audit.rs b/crates/sabia-core/src/audit.rs index b980a5c..decaf20 100644 --- a/crates/sabia-core/src/audit.rs +++ b/crates/sabia-core/src/audit.rs @@ -85,6 +85,26 @@ impl AuditLog { Ok(entry) } + /// Lê as entradas da trilha, opcionalmente só as `limit` mais recentes. + pub fn read_entries(path: impl AsRef, limit: Option) -> Result> { + let file = OpenOptions::new().read(true).open(path)?; + let reader = BufReader::new(file); + let mut entries = Vec::new(); + for line in reader.lines() { + let line = line?; + if line.trim().is_empty() { + continue; + } + entries.push(serde_json::from_str::(&line)?); + } + if let Some(limit) = limit { + if entries.len() > limit { + entries = entries.split_off(entries.len() - limit); + } + } + Ok(entries) + } + /// Verifica a integridade de toda a cadeia; retorna o índice da /// primeira entrada inválida, se houver. pub fn verify(path: impl AsRef) -> Result> { diff --git a/crates/sabia-core/src/crack.rs b/crates/sabia-core/src/crack.rs new file mode 100644 index 0000000..808c728 --- /dev/null +++ b/crates/sabia-core/src/crack.rs @@ -0,0 +1,434 @@ +//! Quebra de WPA/WPA2-PSK por dicionário, em Rust puro. +//! +//! Duas provas são aceitas, como no aircrack-ng/hashcat: +//! +//! * **EAPOL (4-way handshake):** deriva a PMK com PBKDF2-HMAC-SHA1 +//! (4096 iterações), expande a PTK com o PRF do 802.11i e recalcula o MIC da +//! mensagem 2. +//! * **PMKID:** recalcula `HMAC-SHA1(PMK, "PMK Name" || AP || STA)` direto da +//! mensagem 1 — não precisa do handshake completo. +//! +//! É CPU puro, sem GPU: suficiente para a v0.1.0 e para dicionários comuns, +//! mas não substitui o hashcat em ataques de força bruta grandes. + +use crate::dot11::parse_mac; +use crate::error::{Result, SabiaError}; +use crate::handshake::Handshake; +use crate::pcapreader::PcapReader; +use crate::scan::ScanTable; +use hmac::{Hmac, Mac}; +use md5::Md5; +use sha1::Sha1; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::time::Instant; + +type HmacSha1 = Hmac; +type HmacMd5 = Hmac; + +const WPA_ITERATIONS: u32 = 4096; + +#[derive(Debug, Clone)] +pub struct CrackTarget { + pub ssid: String, + pub bssid: [u8; 6], + pub station: [u8; 6], + pub anonce: [u8; 32], + pub snonce: [u8; 32], + pub eapol_msg2: Option>, + pub mic_offset: usize, + pub key_version: u8, + pub pmkid: Option<[u8; 16]>, +} + +#[derive(Debug, Clone, Default, serde::Serialize)] +pub struct CrackReport { + pub password: Option, + pub tried: u64, + pub elapsed_ms: u64, + pub ssid: Option, + pub bssid: Option, + pub station: Option, + pub method: Option, +} + +impl CrackTarget { + pub fn from_handshake(hs: &Handshake) -> Option { + let ssid = hs.ssid.clone().filter(|s| !s.is_empty())?; + let bssid = parse_mac(&hs.bssid)?; + let station = parse_mac(&hs.station)?; + let anonce = hs.anonce?; + let snonce = hs.snonce.unwrap_or([0u8; 32]); + if hs.pmkid.is_none() && (hs.eapol_msg2.is_none() || hs.snonce.is_none()) { + return None; + } + Some(Self { + ssid, + bssid, + station, + anonce, + snonce, + eapol_msg2: hs.eapol_msg2.clone(), + mic_offset: crate::eapol::MIC_OFFSET, + key_version: hs.key_version, + pmkid: hs.pmkid, + }) + } + + pub fn method(&self) -> &'static str { + if self.pmkid.is_some() { + "PMKID" + } else if self.key_version == 1 { + "EAPOL/TKIP" + } else { + "EAPOL/CCMP" + } + } + + pub fn verify(&self, password: &str) -> bool { + let Some(pmk) = pmk_from_candidate(password, self.ssid.as_bytes()) else { + return false; + }; + if let Some(expected) = self.pmkid { + return compute_pmkid(&pmk, &self.bssid, &self.station) == expected; + } + let Some(eapol) = self.eapol_msg2.as_deref() else { + return false; + }; + let ptk = compute_ptk(&pmk, &self.bssid, &self.station, &self.anonce, &self.snonce); + let Some(mic) = compute_mic(self.key_version, &ptk[0..16], eapol, self.mic_offset) else { + return false; + }; + let Some(expected) = eapol.get(self.mic_offset..self.mic_offset + 16) else { + return false; + }; + constant_time_eq(&mic, expected) + } +} + +/// Deriva a PMK (256 bits) de uma frase-senha WPA e do SSID. +pub fn derive_pmk(passphrase: &str, ssid: &str) -> [u8; 32] { + let dk = pbkdf2_sha1(passphrase.as_bytes(), ssid.as_bytes(), WPA_ITERATIONS, 32); + let mut pmk = [0u8; 32]; + pmk.copy_from_slice(&dk[..32]); + pmk +} + +/// Aceita uma frase-senha (8–63 caracteres) ou uma PMK crua em hexadecimal +/// (64 caracteres, como no hashcat/aircrack). +fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32]> { + if candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()) { + return hex_to_32(candidate.as_bytes()); + } + if !(8..=63).contains(&candidate.len()) { + return None; + } + let dk = pbkdf2_sha1(candidate.as_bytes(), ssid, WPA_ITERATIONS, 32); + let mut pmk = [0u8; 32]; + pmk.copy_from_slice(&dk[..32]); + Some(pmk) +} + +pub fn pbkdf2_sha1(password: &[u8], salt: &[u8], iterations: u32, out_len: usize) -> Vec { + let iterations = iterations.max(1); + let mut out = Vec::with_capacity(out_len); + let mut block: u32 = 1; + while out.len() < out_len { + let mut mac = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave"); + mac.update(salt); + mac.update(&block.to_be_bytes()); + let mut u = mac.finalize().into_bytes(); + let mut t = u; + for _ in 1..iterations { + let mut mac = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave"); + mac.update(&u); + u = mac.finalize().into_bytes(); + for (a, b) in t.iter_mut().zip(u.iter()) { + *a ^= *b; + } + } + out.extend_from_slice(&t); + block = block.wrapping_add(1); + } + out.truncate(out_len); + out +} + +fn prf_sha1(key: &[u8], label: &[u8], data: &[u8], out_len: usize) -> Vec { + let mut out = Vec::with_capacity(out_len); + let mut counter: u8 = 0; + while out.len() < out_len { + let mut mac = HmacSha1::new_from_slice(key).expect("HMAC aceita qualquer chave"); + mac.update(label); + mac.update(&[0u8]); + mac.update(data); + mac.update(&[counter]); + out.extend_from_slice(&mac.finalize().into_bytes()); + counter = counter.wrapping_add(1); + } + out.truncate(out_len); + out +} + +/// PTK de 64 bytes: KCK (16) + KEK (16) + TK (16) + 16 de reserva. +pub fn compute_ptk( + pmk: &[u8; 32], + bssid: &[u8; 6], + station: &[u8; 6], + anonce: &[u8; 32], + snonce: &[u8; 32], +) -> [u8; 64] { + let (ap, sta) = if bssid <= station { + (bssid, station) + } else { + (station, bssid) + }; + let (nonce_a, nonce_b) = if anonce <= snonce { + (anonce, snonce) + } else { + (snonce, anonce) + }; + let mut data = Vec::with_capacity(76); + data.extend_from_slice(ap); + data.extend_from_slice(sta); + data.extend_from_slice(nonce_a); + data.extend_from_slice(nonce_b); + let ptk = prf_sha1(pmk, b"Pairwise key expansion", &data, 64); + let mut out = [0u8; 64]; + out.copy_from_slice(&ptk); + out +} + +pub fn compute_pmkid(pmk: &[u8; 32], bssid: &[u8; 6], station: &[u8; 6]) -> [u8; 16] { + let mut mac = HmacSha1::new_from_slice(pmk).expect("HMAC aceita qualquer chave"); + mac.update(b"PMK Name"); + mac.update(bssid); + mac.update(station); + let digest = mac.finalize().into_bytes(); + let mut out = [0u8; 16]; + out.copy_from_slice(&digest[..16]); + out +} + +pub fn compute_mic( + key_version: u8, + kck: &[u8], + eapol: &[u8], + mic_offset: usize, +) -> Option<[u8; 16]> { + if mic_offset + 16 > eapol.len() { + return None; + } + let mut with_zero_mic = eapol.to_vec(); + with_zero_mic[mic_offset..mic_offset + 16].fill(0); + match key_version { + 1 => { + let mut mac = HmacMd5::new_from_slice(kck).ok()?; + mac.update(&with_zero_mic); + let digest = mac.finalize().into_bytes(); + let mut out = [0u8; 16]; + out.copy_from_slice(&digest); + Some(out) + } + 2 => { + let mut mac = HmacSha1::new_from_slice(kck).ok()?; + mac.update(&with_zero_mic); + let digest = mac.finalize().into_bytes(); + let mut out = [0u8; 16]; + out.copy_from_slice(&digest[..16]); + Some(out) + } + _ => None, + } +} + +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 +} + +fn hex_to_32(hex: &[u8]) -> Option<[u8; 32]> { + if hex.len() != 64 { + return None; + } + let mut out = [0u8; 32]; + for (i, byte) in out.iter_mut().enumerate() { + let hi = (hex[i * 2] as char).to_digit(16)?; + let lo = (hex[i * 2 + 1] as char).to_digit(16)?; + *byte = ((hi << 4) | lo) as u8; + } + Some(out) +} + +fn valid_candidate(candidate: &str) -> bool { + (8..=63).contains(&candidate.len()) + || (candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit())) +} + +/// Tenta cada linha do dicionário contra o alvo. `progress` recebe o número +/// de candidatos já processados. +pub fn crack_reader( + target: &CrackTarget, + mut wordlist: R, + mut progress: F, +) -> CrackReport { + let start = Instant::now(); + let mut tried = 0u64; + let mut password = None; + let mut line = String::new(); + loop { + line.clear(); + match wordlist.read_line(&mut line) { + Ok(0) => break, + Ok(_) => {} + Err(_) => break, + } + let candidate = line.trim_end_matches(['\r', '\n']); + if candidate.is_empty() { + continue; + } + tried += 1; + if valid_candidate(candidate) && target.verify(candidate) { + password = Some(candidate.to_string()); + break; + } + if tried % 128 == 0 { + progress(tried); + } + } + progress(tried); + + CrackReport { + password, + tried, + elapsed_ms: start.elapsed().as_millis() as u64, + ssid: Some(target.ssid.clone()), + bssid: Some(crate::dot11::mac_to_string(&target.bssid)), + station: Some(crate::dot11::mac_to_string(&target.station)), + method: Some(target.method().to_string()), + } +} + +pub fn crack_handshake( + hs: &Handshake, + wordlist: impl AsRef, + progress: F, +) -> Result { + let target = CrackTarget::from_handshake(hs).ok_or_else(|| { + SabiaError::InvalidInput( + "handshake sem SSID, PMKID ou par de nonces suficiente para quebrar".into(), + ) + })?; + let file = File::open(wordlist)?; + Ok(crack_reader(&target, BufReader::new(file), progress)) +} + +/// Lê um `.pcap`, monta os handshakes e tenta quebrar o primeiro alvo útil. +pub fn crack_pcap( + pcap: impl AsRef, + wordlist: impl AsRef, + bssid: Option<&str>, + station: Option<&str>, + progress: F, +) -> Result { + let mut reader = PcapReader::open(pcap)?; + if !reader.supports_80211() { + return Err(SabiaError::InvalidPcap( + "link-type não é 802.11 nem radiotap".into(), + )); + } + let mut table = ScanTable::new(); + while let Some(frame) = reader.next_frame()? { + table.ingest(&frame.data); + } + let hs = table + .first_crackable(bssid, station) + .ok_or_else(|| SabiaError::InvalidInput("nenhum handshake quebrável na captura".into()))?; + crack_handshake(hs, wordlist, progress) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testutil::{handshake_frames, synthetic_handshake}; + use std::io::Cursor; + + #[test] + fn pbkdf2_matches_rfc6070_vector() { + let out = pbkdf2_sha1(b"password", b"salt", 1, 20); + assert_eq!(out, hex_bytes("0c60c80f961f0e71f3a9b524af6012062fe037a6")); + let out = pbkdf2_sha1(b"password", b"salt", 2, 20); + assert_eq!(out, hex_bytes("ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957")); + } + + #[test] + fn derives_known_wpa_pmk() { + let pmk = derive_pmk("password", "IEEE"); + assert_eq!( + pmk, + hex_32("f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e") + ); + } + + #[test] + fn cracks_eapol_handshake_with_dictionary() { + let (m1, m2) = handshake_frames("IEEE", "password", 2, false); + let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2); + let target = CrackTarget::from_handshake(&hs).expect("alvo"); + let mut progress_calls = 0; + let report = crack_reader( + &target, + Cursor::new(b"errada\noutra\npassword\nnunca\n".to_vec()), + |_| progress_calls += 1, + ); + assert_eq!(report.password.as_deref(), Some("password")); + assert_eq!(report.tried, 3); + assert_eq!(report.method.as_deref(), Some("EAPOL/CCMP")); + assert!(progress_calls >= 1); + } + + #[test] + fn cracks_pmkid_without_full_handshake() { + let (m1, _) = handshake_frames("IEEE", "password", 2, true); + let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &[]); + let target = CrackTarget::from_handshake(&hs).expect("alvo"); + assert_eq!(target.method(), "PMKID"); + let report = crack_reader(&target, Cursor::new(b"nada\npassword\n".to_vec()), |_| {}); + assert_eq!(report.password.as_deref(), Some("password")); + } + + #[test] + fn accepts_raw_pmk_hex() { + let pmk = derive_pmk("password", "IEEE"); + let hex: String = pmk.iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!(pmk_from_candidate(&hex, b"IEEE"), Some(pmk)); + assert!(pmk_from_candidate("curta", b"IEEE").is_none()); + } + + #[test] + fn rejects_wrong_password() { + let (m1, m2) = handshake_frames("IEEE", "senha-correta", 2, false); + let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2); + let target = CrackTarget::from_handshake(&hs).expect("alvo"); + let report = crack_reader(&target, Cursor::new(b"senha-errada\n".to_vec()), |_| {}); + assert!(report.password.is_none()); + } + + fn hex_bytes(s: &str) -> Vec { + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() + } + + fn hex_32(s: &str) -> [u8; 32] { + hex_bytes(s).try_into().unwrap() + } +} diff --git a/crates/sabia-core/src/doctor.rs b/crates/sabia-core/src/doctor.rs new file mode 100644 index 0000000..7b8313c --- /dev/null +++ b/crates/sabia-core/src/doctor.rs @@ -0,0 +1,135 @@ +//! Diagnóstico do ambiente: privilégios, ferramentas externas, interfaces, +//! suporte a modo monitor e integridade da trilha de auditoria. + +use crate::audit::AuditLog; +use crate::iface::{self, Interface}; +use crate::netlink::{self, command_exists}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct IfaceDiagnosis { + pub name: String, + pub mac: String, + pub mode: String, + pub monitor_supported: bool, + pub driver: Option, + pub up: bool, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct DoctorReport { + pub version: String, + pub platform: String, + pub root: bool, + pub iw: bool, + pub ip: bool, + pub curl: bool, + pub capture_backend: String, + pub audit_path: String, + pub audit_ok: bool, + pub interfaces: Vec, + pub notes: Vec, +} + +pub fn diagnose(version: &str, audit_path: impl AsRef) -> DoctorReport { + let audit_path = audit_path.as_ref().to_path_buf(); + let root = netlink::is_root(); + let iw = command_exists("iw"); + let ip = command_exists("ip"); + let curl = command_exists("curl"); + let (audit_ok, mut notes) = audit_status(&audit_path); + + let all: Vec = iface::list_all().unwrap_or_default(); + let interfaces = all + .into_iter() + .filter(|i| i.phy.is_some()) + .map(|i| IfaceDiagnosis { + monitor_supported: netlink::monitor_supported(&i.name), + driver: netlink::driver_name(&i.name), + name: i.name, + mac: i.mac, + mode: format!("{:?}", i.mode).to_lowercase(), + up: i.up, + }) + .collect::>(); + + if !root { + notes.push( + "sem root: modo monitor, captura e injeção vão falhar (use sudo ou o app desktop)" + .into(), + ); + } + if !iw { + notes.push("`iw` não encontrado no PATH — instale iw/iproute2".into()); + } + if !ip { + notes.push("`ip` não encontrado no PATH — instale iproute2".into()); + } + if interfaces.is_empty() { + notes.push("nenhuma interface Wi-Fi encontrada".into()); + } else if !interfaces.iter().any(|i| i.monitor_supported) { + notes.push("nenhuma interface anuncia suporte a modo monitor".into()); + } + if !curl { + notes.push("`curl` não encontrado — a auto-atualização não funciona".into()); + } + if !audit_ok { + notes.push("a trilha de auditoria existente está corrompida".into()); + } + + DoctorReport { + version: version.to_string(), + platform: format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH), + root, + iw, + ip, + curl, + capture_backend: if cfg!(target_os = "linux") { + "AF_PACKET (Linux)".into() + } else { + "indisponível nesta plataforma".into() + }, + audit_path: audit_path.display().to_string(), + audit_ok, + interfaces, + notes, + } +} + +fn audit_status(path: &PathBuf) -> (bool, Vec) { + let mut notes = Vec::new(); + if !path.exists() { + notes.push(format!( + "trilha de auditoria ainda não criada em {}", + path.display() + )); + return (true, notes); + } + match AuditLog::verify(path) { + Ok(None) => (true, notes), + Ok(Some(idx)) => { + notes.push(format!("cadeia de auditoria rompida na entrada {idx}")); + (false, notes) + } + Err(e) => { + notes.push(format!("não foi possível ler a trilha de auditoria: {e}")); + (false, notes) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn reports_environment_without_panicking() { + let dir = TempDir::new().unwrap(); + let report = diagnose("9.9.9", dir.path().join("nao-existe.log")); + assert_eq!(report.version, "9.9.9"); + assert!(report.audit_ok); + assert!(report.platform.contains('-')); + assert!(!report.capture_backend.is_empty()); + } +} diff --git a/crates/sabia-core/src/dot11.rs b/crates/sabia-core/src/dot11.rs index 8ced17b..2cad044 100644 --- a/crates/sabia-core/src/dot11.rs +++ b/crates/sabia-core/src/dot11.rs @@ -1,5 +1,7 @@ -//! Parser mínimo de quadros 802.11 de gerenciamento (beacon, probe -//! request/response) e detecção de EAPOL (handshake WPA/WPA2). +//! Parser de quadros 802.11 — gerenciamento (beacon, probe, associação, +//! desautenticação) e dados — com leitura dos *information elements* que +//! interessam para auditoria: SSID, canal, segurança (RSN/WPA), WPS, PMF e +//! a localização do payload EAPOL dos handshakes WPA/WPA2. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FrameKind { @@ -7,26 +9,114 @@ pub enum FrameKind { ProbeRequest, ProbeResponse, AssociationRequest, + AssociationResponse, + ReassociationRequest, + Authentication, Deauth, Disassoc, + Action, Data, + Management, + Control, Other, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +impl FrameKind { + pub fn is_beacon_like(self) -> bool { + matches!(self, Self::Beacon | Self::ProbeResponse) + } + + pub fn is_data(self) -> bool { + matches!(self, Self::Data) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] pub enum Security { + #[serde(rename = "Open")] Open, + #[serde(rename = "WEP")] Wep, + #[serde(rename = "WPA")] Wpa, + #[serde(rename = "WPA2")] Wpa2, + #[serde(rename = "WPA2-EAP")] + Wpa2Enterprise, + #[serde(rename = "WPA3")] Wpa3, + #[serde(rename = "WPA3-EAP")] + Wpa3Enterprise, + #[serde(rename = "OWE")] + Owe, + #[serde(rename = "WPA2/WPA3")] + Wpa2Wpa3, +} + +impl Security { + pub fn code(self) -> &'static str { + match self { + Self::Open => "open", + Self::Wep => "wep", + Self::Wpa => "wpa", + Self::Wpa2 => "wpa2", + Self::Wpa2Enterprise => "wpa2-eap", + Self::Wpa3 => "wpa3", + Self::Wpa3Enterprise => "wpa3-eap", + Self::Owe => "owe", + Self::Wpa2Wpa3 => "wpa2-wpa3", + } + } } #[derive(Debug, Clone, Default)] pub struct ManagementInfo { pub ssid: Option, + pub hidden: bool, + pub channel: Option, pub security: Option, - pub channel_ie: Option, + pub cipher: Option, + pub akm: Vec, + pub wps: bool, + pub pmf: bool, + pub country: Option, +} + +#[derive(Debug, Clone, Copy, Default)] +struct RsnInfo { + cipher: Option, + akm: AkmSet, + caps: u16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Cipher { + Wep40, + Wep104, + Tkip, + Ccmp, + Gcmp, + Other, +} + +impl Cipher { + fn label(self) -> &'static str { + match self { + Self::Wep40 | Self::Wep104 => "WEP", + Self::Tkip => "TKIP", + Self::Ccmp => "CCMP", + Self::Gcmp => "GCMP", + Self::Other => "?", + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +struct AkmSet { + psk: bool, + enterprise: bool, + sae: bool, + owe: bool, } pub fn mac_to_string(mac: &[u8]) -> String { @@ -36,15 +126,42 @@ pub fn mac_to_string(mac: &[u8]) -> String { ) } +pub fn parse_mac(s: &str) -> Option<[u8; 6]> { + let mut out = [0u8; 6]; + let mut parts = s.split([':', '-']); + for byte in &mut out { + let part = parts.next()?; + if part.len() != 2 { + return None; + } + *byte = u8::from_str_radix(part, 16).ok()?; + } + if parts.next().is_some() { + return None; + } + Some(out) +} + +pub fn is_broadcast(mac: &[u8; 6]) -> bool { + *mac == [0xff; 6] +} + /// Quadro 802.11 já sem o cabeçalho radiotap. pub struct Frame<'a> { pub kind: FrameKind, + pub fc: u16, + pub to_ds: bool, + pub from_ds: bool, + pub protected: bool, + pub addr1: Option<[u8; 6]>, + pub addr2: Option<[u8; 6]>, + pub addr3: Option<[u8; 6]>, + pub addr4: Option<[u8; 6]>, pub bssid: Option<[u8; 6]>, pub src: Option<[u8; 6]>, pub dst: Option<[u8; 6]>, pub body: &'a [u8], - /// offset, dentro de `body` completo original, de onde começam os - /// "information elements" (só preenchido para beacon/probe response). + pub header_len: usize, ie_offset: usize, } @@ -53,52 +170,120 @@ pub fn parse(buf: &[u8]) -> Option> { return None; } let fc = u16::from_le_bytes([buf[0], buf[1]]); + let version = fc & 0b11; + if version != 0 { + return None; + } let frame_type = (fc >> 2) & 0b11; let subtype = (fc >> 4) & 0b1111; + let to_ds = fc & 0x0100 != 0; + let from_ds = fc & 0x0200 != 0; + let protected = fc & 0x4000 != 0; + let qos = subtype & 0x8 != 0; let kind = match (frame_type, subtype) { (0, 8) => FrameKind::Beacon, (0, 4) => FrameKind::ProbeRequest, (0, 5) => FrameKind::ProbeResponse, (0, 0) => FrameKind::AssociationRequest, + (0, 1) => FrameKind::AssociationResponse, + (0, 2) => FrameKind::ReassociationRequest, + (0, 11) => FrameKind::Authentication, (0, 12) => FrameKind::Deauth, (0, 10) => FrameKind::Disassoc, + (0, 13) => FrameKind::Action, + (0, _) => FrameKind::Management, + (1, _) => FrameKind::Control, (2, _) => FrameKind::Data, _ => FrameKind::Other, }; - if buf.len() < 24 { - // sem addr1/2/3 completos (comum em alguns quadros de controle) + let header_len = match frame_type { + 0 => 24, + 2 => { + let mut len = 24; + if to_ds && from_ds { + len += 6; + } + if qos { + len += 2; + } + if fc & 0x8000 != 0 { + len += 4; // HT Control + } + len + } + _ => 10, + }; + + if buf.len() < header_len { return Some(Frame { kind, + fc, + to_ds, + from_ds, + protected, + addr1: None, + addr2: None, + addr3: None, + addr4: None, bssid: None, src: None, dst: None, body: buf, + header_len: buf.len(), ie_offset: 0, }); } - let addr1: [u8; 6] = buf[4..10].try_into().ok()?; - let addr2: [u8; 6] = buf[10..16].try_into().ok()?; - let addr3: [u8; 6] = buf[16..22].try_into().ok()?; + let mut addr1 = None; + let mut addr2 = None; + let mut addr3 = None; + let mut addr4 = None; + if header_len >= 24 { + addr1 = Some(buf[4..10].try_into().ok()?); + addr2 = Some(buf[10..16].try_into().ok()?); + addr3 = Some(buf[16..22].try_into().ok()?); + } + if header_len >= 30 && to_ds && from_ds { + addr4 = Some(buf[24..30].try_into().ok()?); + } - let (dst, src, bssid) = (addr1, addr2, addr3); + let (bssid, src, dst) = match frame_type { + 0 => (addr3, addr2, addr1), + 2 => match (to_ds, from_ds) { + (false, false) => (addr3, addr2, addr1), + (true, false) => (addr1, addr2, addr3), + (false, true) => (addr2, addr3, addr1), + (true, true) => (None, addr4, addr3), + }, + _ => (None, None, None), + }; - // Para beacon/probe response, corpo fixo de 12 bytes (timestamp + - // interval + capability) antecede os information elements. - let ie_offset = if matches!(kind, FrameKind::Beacon | FrameKind::ProbeResponse) { - 24 + 12 - } else { - 24 + let ie_offset = match kind { + FrameKind::Beacon | FrameKind::ProbeResponse => 24 + 12, + FrameKind::ProbeRequest => 24, + FrameKind::AssociationRequest => 24 + 4, + FrameKind::ReassociationRequest => 24 + 10, + FrameKind::AssociationResponse => 24 + 6, + _ => 0, }; Some(Frame { kind, - bssid: Some(bssid), - src: Some(src), - dst: Some(dst), + fc, + to_ds, + from_ds, + protected, + addr1, + addr2, + addr3, + addr4, + bssid, + src, + dst, body: buf, + header_len, ie_offset, }) } @@ -112,29 +297,33 @@ impl<'a> Frame<'a> { self.src.map(|m| mac_to_string(&m)) } - /// Faz parsing dos "information elements" (TLV) de um beacon/probe - /// response, extraindo SSID, canal e indício de cifragem. + pub fn dst_str(&self) -> Option { + self.dst.map(|m| mac_to_string(&m)) + } + + /// Slice dos information elements, quando o quadro tem um. + pub fn ies(&self) -> Option<&'a [u8]> { + if self.ie_offset == 0 || self.ie_offset >= self.body.len() { + None + } else { + Some(&self.body[self.ie_offset..]) + } + } + + /// Faz parsing dos information elements (TLV), extraindo SSID, canal, + /// cifra/AKM e indicadores de WPS e PMF. pub fn management_info(&self) -> ManagementInfo { let mut info = ManagementInfo::default(); - if self.ie_offset == 0 || self.ie_offset >= self.body.len() { + let Some(ies) = self.ies() else { return info; - } - let privacy = { - // bit "Privacy" no campo capability, 2 bytes antes do fim do - // cabeçalho fixo (offset 24+10, 2 bytes). - if self.body.len() >= 24 + 12 { - let cap = u16::from_le_bytes([self.body[24 + 10], self.body[24 + 11]]); - cap & 0x0010 != 0 - } else { - false - } }; + let privacy = self.privacy_bit(); let mut rsn_present = false; let mut wpa1_present = false; + let mut rsn = RsnInfo::default(); - let ies = &self.body[self.ie_offset..]; - let mut i = 0; + let mut i = 0usize; while i + 2 <= ies.len() { let tag = ies[i]; let len = ies[i + 1] as usize; @@ -145,58 +334,348 @@ impl<'a> Frame<'a> { let val = &ies[start..start + len]; match tag { 0 => { - // SSID; pode ser "oculto" (len 0 ou só zeros) - if len > 0 && val.iter().any(|&b| b != 0) { - info.ssid = Some(String::from_utf8_lossy(val).into_owned()); - } else { + let trimmed: Vec = val.iter().copied().filter(|b| *b != 0).collect(); + if trimmed.is_empty() { info.ssid = Some(String::new()); + info.hidden = true; + } else { + info.ssid = Some(String::from_utf8_lossy(&trimmed).into_owned()); } } - 3 => { - if len == 1 { - info.channel_ie = Some(val[0] as u32); + 3 if len == 1 => info.channel = Some(val[0] as u32), + 7 if len >= 2 => { + info.country = Some(String::from_utf8_lossy(&val[0..2]).into_owned()) + } + 48 => { + rsn_present = true; + if let Some(parsed) = parse_rsn(val) { + rsn = parsed; } } - 48 => rsn_present = true, // RSN IE = WPA2/WPA3 - // vendor-specific; OUI 00:50:F2 tipo 1 = WPA1 - 221 if len >= 4 && val[0..3] == [0x00, 0x50, 0xF2] && val[3] == 1 => { - wpa1_present = true; - } + 221 if len >= 4 && val[0..3] == [0x00, 0x50, 0xF2] => match val[3] { + 1 => wpa1_present = true, + 4 => info.wps = true, + _ => {} + }, _ => {} } i = start + len; } - info.security = Some(if rsn_present { - Security::Wpa2 - } else if wpa1_present { - Security::Wpa - } else if privacy { - Security::Wep - } else { - Security::Open - }); + info.pmf = rsn.caps & 0x00c0 != 0; + if rsn_present { + info.cipher = rsn.cipher.map(|c| c.label().to_string()); + info.akm = rsn.akm.labels(); + } + info.security = Some(classify(rsn_present, wpa1_present, privacy, rsn)); info } + + fn privacy_bit(&self) -> bool { + match self.kind { + FrameKind::Beacon | FrameKind::ProbeResponse => { + self.body.len() >= 24 + 12 + && u16::from_le_bytes([self.body[24 + 10], self.body[24 + 11]]) & 0x0010 != 0 + } + FrameKind::AssociationRequest => { + self.body.len() >= 26 + && u16::from_le_bytes([self.body[24], self.body[25]]) & 0x0010 != 0 + } + _ => false, + } + } + + /// Payload EAPOL (handshake WPA) de um quadro de dados não protegido. + /// O slice começa no cabeçalho EAPOL (versão, tipo, tamanho). + pub fn eapol(&self) -> Option<&'a [u8]> { + if self.protected || !self.kind.is_data() { + return None; + } + let llc = self.body.get(self.header_len..self.header_len + 8)?; + if llc[0] != 0xAA || llc[1] != 0xAA || llc[2] != 0x03 { + return None; + } + if u16::from_be_bytes([llc[6], llc[7]]) != 0x888E { + return None; + } + let start = self.header_len + 8; + let header = self.body.get(start..start + 4)?; + let len = u16::from_be_bytes([header[2], header[3]]) as usize; + let end = (start + 4 + len).min(self.body.len()); + self.body.get(start..end) + } } -/// Detecta se um quadro de dados carrega EAPOL (handshake 4-way), olhando o -/// cabeçalho LLC/SNAP (ethertype 0x888E) após o cabeçalho 802.11 de dados. +/// Mantido para chamadores antigos: há um EAPOL neste quadro? pub fn is_eapol(body: &[u8]) -> bool { - // Cabeçalho de dados pode ter 24 ou 26 bytes (QoS); LLC/SNAP tem 8 - // bytes (AA AA 03 00 00 00) + ethertype de 2 bytes. - for hdr_len in [24usize, 26] { - if body.len() < hdr_len + 8 { - continue; + parse(body).and_then(|f| f.eapol().map(|_| ())).is_some() +} + +fn classify(rsn_present: bool, wpa1_present: bool, privacy: bool, rsn: RsnInfo) -> Security { + if rsn_present { + let akm = rsn.akm; + if akm.owe { + return Security::Owe; } - let llc = &body[hdr_len..hdr_len + 8]; - if llc[0] == 0xAA && llc[1] == 0xAA && llc[2] == 0x03 { - let ethertype = u16::from_be_bytes([llc[6], llc[7]]); - if ethertype == 0x888E { - return true; + if akm.sae && akm.enterprise { + return Security::Wpa3Enterprise; + } + if akm.sae && akm.psk { + return Security::Wpa2Wpa3; + } + if akm.sae { + return Security::Wpa3; + } + if akm.enterprise { + return Security::Wpa2Enterprise; + } + return Security::Wpa2; + } + if wpa1_present { + return Security::Wpa; + } + if privacy { + return Security::Wep; + } + Security::Open +} + +fn parse_rsn(val: &[u8]) -> Option { + if val.len() < 8 { + return None; + } + if u16::from_le_bytes([val[0], val[1]]) != 1 { + return None; + } + let group = cipher_from_suite(&val[2..6]); + let mut pos = 6usize; + let pairwise_count = u16::from_le_bytes([val[pos], val[pos + 1]]) as usize; + pos += 2; + let mut cipher = group; + for _ in 0..pairwise_count { + if pos + 4 > val.len() { + return None; + } + let suite = cipher_from_suite(&val[pos..pos + 4]); + pos += 4; + if let Some(suite) = suite { + if suite != Cipher::Other && cipher == Some(Cipher::Other) { + cipher = Some(suite); + } + if matches!(suite, Cipher::Ccmp | Cipher::Gcmp) { + cipher = Some(suite); } } } - false + if pos + 2 > val.len() { + return Some(RsnInfo { + cipher, + ..Default::default() + }); + } + let akm_count = u16::from_le_bytes([val[pos], val[pos + 1]]) as usize; + pos += 2; + let mut akm = AkmSet::default(); + for _ in 0..akm_count { + if pos + 4 > val.len() { + break; + } + if val[pos..pos + 3] == [0x00, 0x0f, 0xac] { + match val[pos + 3] { + 1 | 3 | 5 | 11 => akm.enterprise = true, + 2 | 4 | 6 => akm.psk = true, + 8 | 9 | 24 => akm.sae = true, + 18 => akm.owe = true, + _ => {} + } + } + pos += 4; + } + let caps = if pos + 2 <= val.len() { + u16::from_le_bytes([val[pos], val[pos + 1]]) + } else { + 0 + }; + Some(RsnInfo { cipher, akm, caps }) +} + +fn cipher_from_suite(suite: &[u8]) -> Option { + if suite.len() < 4 { + return None; + } + if suite[0..3] != [0x00, 0x0f, 0xac] { + return Some(Cipher::Other); + } + Some(match suite[3] { + 1 => Cipher::Wep40, + 2 => Cipher::Tkip, + 4 => Cipher::Ccmp, + 5 => Cipher::Wep104, + 8 | 9 => Cipher::Gcmp, + _ => Cipher::Other, + }) +} + +impl AkmSet { + fn labels(self) -> Vec { + let mut out = Vec::new(); + if self.psk { + out.push("PSK".into()); + } + if self.sae { + out.push("SAE".into()); + } + if self.enterprise { + out.push("802.1X".into()); + } + if self.owe { + out.push("OWE".into()); + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn mac(b: u8) -> [u8; 6] { + [b, b, b, b, b, b] + } + + fn beacon_with_ies(ies: &[u8]) -> Vec { + let mut f = Vec::new(); + f.extend_from_slice(&0x0080u16.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&[0xff; 6]); + f.extend_from_slice(&mac(0x11)); + f.extend_from_slice(&mac(0x11)); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&[0u8; 8]); // timestamp + f.extend_from_slice(&100u16.to_le_bytes()); // beacon interval + f.extend_from_slice(&0x0031u16.to_le_bytes()); // capability: ESS + privacy + f.extend_from_slice(ies); + f + } + + fn ie(tag: u8, val: &[u8]) -> Vec { + let mut v = vec![tag, val.len() as u8]; + v.extend_from_slice(val); + v + } + + fn rsn_ie(akms: &[u8]) -> Vec { + let mut val = Vec::new(); + val.extend_from_slice(&1u16.to_le_bytes()); + val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); // group CCMP + val.extend_from_slice(&1u16.to_le_bytes()); + val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); // pairwise CCMP + val.extend_from_slice(&(akms.len() as u16).to_le_bytes()); + for akm in akms { + val.extend_from_slice(&[0x00, 0x0f, 0xac, *akm]); + } + val.extend_from_slice(&0x0040u16.to_le_bytes()); // MFPC + ie(48, &val) + } + + fn all_ie(ies: &[Vec]) -> Vec { + ies.concat() + } + + #[test] + fn parses_wpa2_psk_beacon() { + let ies = all_ie(&[ie(0, b"MinhaCasa"), ie(3, &[6]), rsn_ie(&[2])]); + let frame = beacon_with_ies(&ies); + let f = parse(&frame).expect("frame"); + assert_eq!(f.kind, FrameKind::Beacon); + assert_eq!(f.bssid_str().as_deref(), Some("11:11:11:11:11:11")); + let info = f.management_info(); + assert_eq!(info.ssid.as_deref(), Some("MinhaCasa")); + assert_eq!(info.channel, Some(6)); + assert_eq!(info.security, Some(Security::Wpa2)); + assert_eq!(info.cipher.as_deref(), Some("CCMP")); + assert_eq!(info.akm, vec!["PSK"]); + assert!(info.pmf); + assert!(!info.wps); + } + + #[test] + fn classifies_wpa3_transition_enterprise_and_owe() { + let cases = [ + (vec![8], Security::Wpa3), + (vec![2, 8], Security::Wpa2Wpa3), + (vec![1], Security::Wpa2Enterprise), + (vec![18], Security::Owe), + ]; + for (akms, expected) in cases { + let ies = all_ie(&[ie(0, b"x"), rsn_ie(&akms)]); + let frame = beacon_with_ies(&ies); + let f = parse(&frame).expect("frame"); + assert_eq!(f.management_info().security, Some(expected), "{akms:?}"); + } + } + + #[test] + fn detects_hidden_ssid_and_wps() { + let wps = ie(221, &[0x00, 0x50, 0xF2, 4, 0x10, 0x4a]); + let ies = all_ie(&[ie(0, &[]), wps, rsn_ie(&[2])]); + let frame = beacon_with_ies(&ies); + let f = parse(&frame).expect("frame"); + let info = f.management_info(); + assert!(info.hidden); + assert_eq!(info.ssid.as_deref(), Some("")); + assert!(info.wps); + } + + #[test] + fn parses_directed_data_and_eapol_slice() { + let mut f = Vec::new(); + f.extend_from_slice(&0x0108u16.to_le_bytes()); // data, ToDS + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&mac(0x11)); // BSSID + f.extend_from_slice(&mac(0x22)); // station + f.extend_from_slice(&mac(0x33)); // DA + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x88, 0x8e]); + let eapol = [0x02u8, 0x03, 0x00, 0x05, 1, 2, 3, 4, 5]; + f.extend_from_slice(&eapol); + let parsed = parse(&f).expect("frame"); + assert!(parsed.to_ds); + assert_eq!(parsed.header_len, 24); + assert_eq!(parsed.bssid_str().as_deref(), Some("11:11:11:11:11:11")); + assert_eq!(parsed.src_str().as_deref(), Some("22:22:22:22:22:22")); + assert_eq!(parsed.eapol(), Some(&eapol[..])); + } + + #[test] + fn qos_and_protected_data_are_handled() { + let mut f = Vec::new(); + f.extend_from_slice(&0x4088u16.to_le_bytes()); // QoS data, protected + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&mac(0x11)); + f.extend_from_slice(&mac(0x22)); + f.extend_from_slice(&mac(0x33)); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&[0u8; 2]); // QoS control + f.extend_from_slice(&[0u8; 32]); // ciphertext + let parsed = parse(&f).expect("frame"); + assert!(parsed.protected); + assert_eq!(parsed.header_len, 26); + assert!(parsed.eapol().is_none()); + } + + #[test] + fn parses_mac_strings() { + assert_eq!( + parse_mac("00:11:22:33:44:55"), + Some([0, 0x11, 0x22, 0x33, 0x44, 0x55]) + ); + assert_eq!( + parse_mac("00-11-22-33-44-55"), + Some([0, 0x11, 0x22, 0x33, 0x44, 0x55]) + ); + assert!(parse_mac("00:11:22").is_none()); + assert!(parse_mac("zz:11:22:33:44:55").is_none()); + } } diff --git a/crates/sabia-core/src/eapol.rs b/crates/sabia-core/src/eapol.rs new file mode 100644 index 0000000..b28ae6d --- /dev/null +++ b/crates/sabia-core/src/eapol.rs @@ -0,0 +1,194 @@ +//! Parser do handshake WPA/WPA2 (EAPOL-Key, IEEE 802.1X). +//! +//! Lê as quatro mensagens do 4-way handshake, identifica o número de cada +//! uma, extrai nonces, MIC, versão de chave e PMKID — matéria-prima para +//! montar um alvo de quebra por dicionário em [`crate::crack`]. + +pub const EAPOL_TYPE_KEY: u8 = 3; + +const KEY_INFO_PAIRWISE: u16 = 0x0008; +const KEY_INFO_INSTALL: u16 = 0x0040; +const KEY_INFO_ACK: u16 = 0x0080; +const KEY_INFO_MIC: u16 = 0x0100; +const KEY_INFO_SECURE: u16 = 0x0200; + +/// Deslocamento absoluto do campo MIC dentro do quadro EAPOL (já incluindo +/// o cabeçalho de 4 bytes). +pub const MIC_OFFSET: usize = 81; +pub const KEY_DATA_OFFSET: usize = 99; +pub const MIN_KEY_FRAME_LEN: usize = 99; + +#[derive(Debug, Clone)] +pub struct EapolKey { + pub version: u8, + pub descriptor_type: u8, + /// 1, 2, 3 ou 4; 0 quando não é uma das mensagens do 4-way handshake. + pub message: u8, + pub key_info: u16, + /// Versão do descritor: 1 = TKIP, 2 = CCMP, 3 = AES-CMAC. + pub key_version: u8, + pub pairwise: bool, + pub replay_counter: u64, + pub nonce: [u8; 32], + pub mic: [u8; 16], + pub key_data: Vec, + pub pmkid: Option<[u8; 16]>, + /// Quadro EAPOL completo (cabeçalho + descritor + dados), com o MIC no + /// lugar — usado para recalcular o MIC durante a quebra. + pub frame: Vec, +} + +pub fn parse(buf: &[u8]) -> Option { + if buf.len() < 4 || buf[1] != EAPOL_TYPE_KEY { + return None; + } + let version = buf[0]; + let declared = u16::from_be_bytes([buf[2], buf[3]]) as usize; + let total = 4 + declared; + if total < MIN_KEY_FRAME_LEN || buf.len() < total { + return None; + } + let frame = buf[..total].to_vec(); + + let descriptor_type = frame[4]; + // 2 = RSN (WPA2/WPA3), 254 = WPA1. Outros descritores não interessam. + if descriptor_type != 2 && descriptor_type != 254 { + return None; + } + let key_info = u16::from_le_bytes([frame[5], frame[6]]); + let key_version = (key_info & 0x0007) as u8; + let pairwise = key_info & KEY_INFO_PAIRWISE != 0; + let ack = key_info & KEY_INFO_ACK != 0; + let mic = key_info & KEY_INFO_MIC != 0; + let install = key_info & KEY_INFO_INSTALL != 0; + let secure = key_info & KEY_INFO_SECURE != 0; + + let message = if ack && !mic { + 1 + } else if !ack && mic && !secure { + 2 + } else if ack && mic && (install || !secure) { + 3 + } else if !ack && mic && secure { + 4 + } else { + 0 + }; + + let replay_counter = u64::from_le_bytes(frame[9..17].try_into().ok()?); + let mut nonce = [0u8; 32]; + nonce.copy_from_slice(&frame[17..49]); + let mut mic_bytes = [0u8; 16]; + mic_bytes.copy_from_slice(&frame[MIC_OFFSET..MIC_OFFSET + 16]); + let key_data_len = u16::from_le_bytes([frame[97], frame[98]]) as usize; + let data_end = KEY_DATA_OFFSET.checked_add(key_data_len)?; + if data_end > frame.len() { + return None; + } + let key_data = frame[KEY_DATA_OFFSET..data_end].to_vec(); + let pmkid = parse_pmkid_kde(&key_data); + + Some(EapolKey { + version, + descriptor_type, + message, + key_info, + key_version, + pairwise, + replay_counter, + nonce, + mic: mic_bytes, + key_data, + pmkid, + frame, + }) +} + +/// PMKID KDE: `dd 14 00 0f ac 04` + 16 bytes. Pode vir em qualquer posição +/// dos dados da chave (normalmente na mensagem 1). +pub fn parse_pmkid_kde(data: &[u8]) -> Option<[u8; 16]> { + let mut i = 0usize; + while i + 2 <= data.len() { + let tag = data[i]; + let len = data[i + 1] as usize; + let start = i + 2; + if start + len > data.len() { + break; + } + if tag == 0xdd && len >= 20 && data[start..start + 4] == [0x00, 0x0f, 0xac, 0x04] { + let mut pmkid = [0u8; 16]; + pmkid.copy_from_slice(&data[start + 4..start + 20]); + return Some(pmkid); + } + i = start + len; + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + pub(crate) fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> Vec { + let mut key_info = keyver as u16 | KEY_INFO_PAIRWISE; + match message { + 1 => key_info |= KEY_INFO_ACK, + 2 => key_info |= KEY_INFO_MIC, + 3 => key_info |= KEY_INFO_ACK | KEY_INFO_MIC | KEY_INFO_INSTALL, + 4 => key_info |= KEY_INFO_MIC | KEY_INFO_SECURE, + _ => {} + } + let total = KEY_DATA_OFFSET + key_data.len(); + let mut f = vec![0u8; total]; + f[0] = 2; // EAPOL version + f[1] = EAPOL_TYPE_KEY; + f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes()); + f[4] = 2; // RSN descriptor + f[5..7].copy_from_slice(&key_info.to_le_bytes()); + f[7..9].copy_from_slice(&16u16.to_le_bytes()); + f[9..17].copy_from_slice(&1u64.to_le_bytes()); + f[17..49].copy_from_slice(&nonce); + for (i, b) in f[MIC_OFFSET..MIC_OFFSET + 16].iter_mut().enumerate() { + *b = i as u8; + } + f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes()); + f[KEY_DATA_OFFSET..].copy_from_slice(key_data); + f + } + + #[test] + fn identifies_all_four_messages() { + for msg in 1..=4u8 { + let frame = key_frame(msg, 2, [msg; 32], &[]); + let parsed = parse(&frame).expect("eapol"); + assert_eq!(parsed.message, msg); + assert_eq!(parsed.key_version, 2); + assert!(parsed.pairwise); + assert_eq!(parsed.nonce, [msg; 32]); + assert_eq!(parsed.mic, { + let mut m = [0u8; 16]; + for (i, b) in m.iter_mut().enumerate() { + *b = i as u8; + } + m + }); + assert_eq!(parsed.frame, frame); + } + } + + #[test] + fn extracts_pmkid_from_key_data() { + let mut data = vec![0xdd, 20, 0x00, 0x0f, 0xac, 0x04]; + data.extend_from_slice(&[0xab; 16]); + let frame = key_frame(1, 2, [0x11; 32], &data); + let parsed = parse(&frame).expect("eapol"); + assert_eq!(parsed.pmkid, Some([0xab; 16])); + } + + #[test] + fn rejects_non_key_and_truncated_frames() { + assert!(parse(&[2, 0, 0, 0]).is_none()); + let frame = key_frame(2, 2, [0; 32], &[]); + assert!(parse(&frame[..frame.len() - 1]).is_none()); + } +} diff --git a/crates/sabia-core/src/engine.rs b/crates/sabia-core/src/engine.rs index b6cc7ec..10cbad0 100644 --- a/crates/sabia-core/src/engine.rs +++ b/crates/sabia-core/src/engine.rs @@ -1,26 +1,116 @@ //! Ponte única entre o núcleo e as duas interfaces (servidor local da CLI e //! o app desktop Tauri): ambos chamam exatamente os mesmos métodos, então //! qualquer coisa que funcione em um funciona idêntico no outro. +//! +//! Sessões de captura, jobs de quebra, exportação de handshakes e deauth +//! ficam todos aqui — a CLI e a GUI não reimplementam nada. use crate::audit::AuditLog; -use crate::error::Result; -use crate::iface::{self, Interface}; -use crate::netlink; -use crate::scan::{AccessPoint, ScanTable}; +use crate::crack::{self, CrackReport}; +use crate::doctor::{self, DoctorReport}; +use crate::error::{Result, SabiaError}; +use crate::handshake::Handshake; +use crate::iface::{self, Interface, Mode}; +use crate::netlink::{self, ChannelInfo}; +use crate::pcapreader::PcapReader; +use crate::pcapwriter::PcapWriter; +use crate::scan::{AccessPoint, Client, ScanSnapshot, ScanTable}; use crate::update::{self, UpdateStatus}; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScanOptions { + /// Canal fixo; sem ele, percorre os canais suportados. + pub channel: Option, + /// `2.4`, `5`, `6` ou `all` (usado quando `channel` é `None`). + pub band: Option, + /// Tempo em cada canal ao pular. + pub hop_ms: u64, + /// Salva os quadros capturados neste `.pcap`. + pub pcap_out: Option, + /// Encerra sozinho depois de N segundos. + pub duration_secs: Option, +} + +impl Default for ScanOptions { + fn default() -> Self { + Self { + channel: None, + band: None, + hop_ms: 350, + pcap_out: None, + duration_secs: None, + } + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct SessionInfo { + pub iface: String, + pub running: bool, + pub channel: Option, + pub channels: Vec, + pub frames: u64, + pub aps: usize, + pub clients: usize, + pub handshakes: usize, + pub started_unix: u64, + pub pcap_out: Option, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct CrackJobInfo { + pub id: String, + pub iface: String, + pub bssid: Option, + pub station: Option, + pub status: String, + pub tried: u64, + pub started_unix: u64, + pub finished_unix: Option, + pub report: Option, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct ExportReport { + pub path: String, + pub frames: usize, + pub handshakes: usize, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DeauthReport { + pub iface: String, + pub bssid: String, + pub station: String, + pub reason: u16, + pub frames_sent: usize, +} struct Session { table: Arc>, running: Arc, + channel: Arc>>, + channels: Vec, + started_unix: u64, + pcap_out: Option, + error: Arc>>, + handle: Option>, } pub struct Engine { sessions: Mutex>, + jobs: Mutex>>>, + job_seq: AtomicU64, audit_path: PathBuf, + audit_lock: Arc>, } impl Default for Engine { @@ -31,9 +121,16 @@ impl Default for Engine { impl Engine { pub fn new() -> Self { + Self::with_audit_path(PathBuf::from(".sabia-audit.log")) + } + + pub fn with_audit_path(path: impl Into) -> Self { Self { sessions: Mutex::new(HashMap::new()), - audit_path: PathBuf::from(".sabia-audit.log"), + jobs: Mutex::new(HashMap::new()), + job_seq: AtomicU64::new(0), + audit_path: path.into(), + audit_lock: Arc::new(Mutex::new(())), } } @@ -41,91 +138,597 @@ impl Engine { env!("CARGO_PKG_VERSION") } + pub fn audit_path(&self) -> &Path { + &self.audit_path + } + pub fn list_ifaces(&self) -> Result> { iface::list_wireless() } + pub fn doctor(&self) -> DoctorReport { + doctor::diagnose(self.version(), &self.audit_path) + } + pub fn set_monitor(&self, name: &str, enable: bool) -> Result<()> { - netlink::set_monitor_mode(name, enable)?; - if let Ok(mut log) = AuditLog::open(&self.audit_path) { - let _ = log.record("monitor_mode", name, &format!("{{\"enable\":{enable}}}")); + iface::get(name)?; + if !enable { + self.stop_scan(name); } + netlink::set_monitor_mode(name, enable)?; + self.log( + "monitor_mode", + name, + serde_json::json!({ "enable": enable }), + ); Ok(()) } - #[cfg(target_os = "linux")] - pub fn start_scan(&self, name: &str) -> Result<()> { - use crate::capture::RawCapture; - use std::thread; - use std::time::Duration; + pub fn channels(&self, name: &str) -> Result> { + netlink::channels(name) + } - let mut sessions = self.sessions.lock().unwrap(); - if sessions.contains_key(name) { - return Ok(()); + pub fn set_channel(&self, name: &str, channel: u32) -> Result<()> { + netlink::set_channel(name, channel)?; + if let Some(session) = self.sessions.lock().unwrap().get(name) { + *session.channel.lock().unwrap() = Some(channel); } + self.log( + "set_channel", + name, + serde_json::json!({ "channel": channel }), + ); + Ok(()) + } + + pub fn start_scan(&self, name: &str) -> Result<()> { + self.start_scan_with(name, ScanOptions::default()) + } + + #[cfg(target_os = "linux")] + pub fn start_scan_with(&self, name: &str, options: ScanOptions) -> Result<()> { + use crate::capture::RawCapture; + + let info = iface::get(name)?; + if info.mode == Mode::Managed { + return Err(SabiaError::InvalidInput(format!( + "{name} está em modo managed; ative o modo monitor antes de capturar" + ))); + } + + let cap = RawCapture::open(name)?; + cap.set_read_timeout(Duration::from_millis(200))?; + + let writer = match options.pcap_out.as_ref() { + Some(path) => Some(PcapWriter::create(path)?), + None => None, + }; + + let channels = match options.channel { + Some(channel) => vec![channel], + None => netlink::channel_plan(name, options.band.as_deref()).unwrap_or_default(), + }; + let hop = Duration::from_millis(options.hop_ms.clamp(100, 5_000)); + + // Substitui uma sessão anterior (se houver) só depois que a nova + // captura abriu, para não derrubar a antiga se algo falhar. + self.stop_scan(name); + let table = Arc::new(Mutex::new(ScanTable::new())); let running = Arc::new(AtomicBool::new(true)); + let channel = Arc::new(Mutex::new(None)); + let error = Arc::new(Mutex::new(None)); + let started_unix = unix_now(); + let pcap_out = options.pcap_out.clone(); let t = table.clone(); let r = running.clone(); + let ch = channel.clone(); + let err_slot = error.clone(); let iface_name = name.to_string(); - thread::spawn(move || { - let cap = match RawCapture::open(&iface_name) { - Ok(c) => c, - Err(e) => { - eprintln!("falha ao abrir captura em {iface_name}: {e}"); - return; - } - }; - let _ = cap.set_read_timeout(Duration::from_millis(500)); - let channels = netlink::supported_channels(&iface_name).unwrap_or_default(); + let audit_path = self.audit_path.clone(); + let audit_lock = self.audit_lock.clone(); + let deadline = options + .duration_secs + .map(|secs| Instant::now() + Duration::from_secs(secs.max(1))); + + let thread_channels = channels.clone(); + let handle = std::thread::spawn(move || { + let channels = thread_channels; + let mut writer = writer; let mut idx = 0usize; - let mut last_hop = std::time::Instant::now(); + let mut last_hop = Instant::now(); let mut buf = [0u8; 65535]; - while r.load(Ordering::SeqCst) { - if !channels.is_empty() && last_hop.elapsed() > Duration::from_millis(300) { - idx = (idx + 1) % channels.len(); - let _ = netlink::set_channel(&iface_name, channels[idx]); - last_hop = std::time::Instant::now(); + + if !channels.is_empty() { + let first = channels[0]; + if netlink::set_channel(&iface_name, first).is_ok() { + *ch.lock().unwrap() = Some(first); } - if let Ok(Some(n)) = cap.recv(&mut buf) { - let mut table = t.lock().unwrap(); - table.ingest(&buf[..n]); + } + + while r.load(Ordering::SeqCst) { + if deadline.is_some_and(|d| Instant::now() >= d) { + break; + } + if channels.len() > 1 && last_hop.elapsed() >= hop { + idx = (idx + 1) % channels.len(); + let next = channels[idx]; + if netlink::set_channel(&iface_name, next).is_ok() { + *ch.lock().unwrap() = Some(next); + } + last_hop = Instant::now(); + } + + match cap.recv(&mut buf) { + Ok(Some(n)) => { + let frame = &buf[..n]; + t.lock().unwrap().ingest(frame); + if let Some(w) = writer.as_mut() { + if let Err(e) = w.write_frame(frame) { + *err_slot.lock().unwrap() = Some(e.to_string()); + break; + } + } + } + Ok(None) => {} + Err(e) => { + *err_slot.lock().unwrap() = Some(e.to_string()); + break; + } + } + } + + if let Some(w) = writer.as_mut() { + let _ = w.flush(); + } + r.store(false, Ordering::SeqCst); + + let stats = t.lock().unwrap().stats(); + append_audit( + &audit_path, + &audit_lock, + "scan_stop", + &iface_name, + &serde_json::json!({ + "aps": stats.aps, + "clients": stats.clients, + "handshakes": stats.handshakes, + "frames": stats.frames, + }), + ); + }); + + self.sessions.lock().unwrap().insert( + name.to_string(), + Session { + table, + running, + channel, + channels, + started_unix, + pcap_out, + error, + handle: Some(handle), + }, + ); + + self.log( + "scan_start", + name, + serde_json::json!({ "options": options }), + ); + Ok(()) + } + + #[cfg(not(target_os = "linux"))] + pub fn start_scan_with(&self, _name: &str, _options: ScanOptions) -> Result<()> { + Err(SabiaError::UnsupportedPlatform) + } + + pub fn stop_scan(&self, name: &str) { + let handle = { + let mut sessions = self.sessions.lock().unwrap(); + match sessions.get_mut(name) { + Some(session) => { + session.running.store(false, Ordering::SeqCst); + session.handle.take() + } + None => None, + } + }; + if let Some(handle) = handle { + let _ = handle.join(); + } + } + + pub fn list_sessions(&self) -> Vec { + let sessions = self.sessions.lock().unwrap(); + let mut out: Vec = sessions + .iter() + .map(|(name, session)| { + let stats = session.table.lock().unwrap().stats(); + SessionInfo { + iface: name.clone(), + running: session.running.load(Ordering::SeqCst), + channel: *session.channel.lock().unwrap(), + channels: session.channels.clone(), + frames: stats.frames, + aps: stats.aps, + clients: stats.clients, + handshakes: stats.handshakes, + started_unix: session.started_unix, + pcap_out: session.pcap_out.as_ref().map(|p| p.display().to_string()), + error: session.error.lock().unwrap().clone(), + } + }) + .collect(); + out.sort_by(|a, b| a.iface.cmp(&b.iface)); + out + } + + pub fn scan_snapshot(&self, name: &str) -> Result { + let sessions = self.sessions.lock().unwrap(); + let session = sessions.get(name).ok_or_else(|| { + SabiaError::InvalidInput(format!("nenhuma sessão de captura em {name}")) + })?; + let table = session.table.lock().unwrap(); + let snapshot = ScanSnapshot { + iface: name.to_string(), + running: session.running.load(Ordering::SeqCst), + channel: *session.channel.lock().unwrap(), + stats: table.stats(), + access_points: table.access_points(), + clients: table.clients(), + handshakes: table.handshakes(), + }; + drop(table); + drop(sessions); + Ok(snapshot) + } + + /// Compatibilidade com a API antiga: só os APs da sessão. + pub fn scan_results(&self, name: &str) -> Vec { + self.sessions + .lock() + .unwrap() + .get(name) + .map(|s| s.table.lock().unwrap().access_points()) + .unwrap_or_default() + } + + pub fn scan_clients(&self, name: &str) -> Vec { + self.sessions + .lock() + .unwrap() + .get(name) + .map(|s| s.table.lock().unwrap().clients()) + .unwrap_or_default() + } + + /// Analisa um `.pcap` offline e devolve o mesmo snapshot da captura ao + /// vivo (sem sessão e sem tocar no rádio). + pub fn analyze_pcap(&self, path: impl AsRef) -> Result { + let path = path.as_ref(); + let mut reader = PcapReader::open(path)?; + if !reader.supports_80211() { + return Err(SabiaError::InvalidPcap( + "link-type não é 802.11 nem radiotap".into(), + )); + } + let mut table = ScanTable::new(); + while let Some(frame) = reader.next_frame()? { + table.ingest(&frame.data); + } + Ok(ScanSnapshot { + iface: path.display().to_string(), + running: false, + channel: None, + stats: table.stats(), + access_points: table.access_points(), + clients: table.clients(), + handshakes: table.handshakes(), + }) + } + + pub fn handshakes(&self, name: Option<&str>) -> Vec { + let sessions = self.sessions.lock().unwrap(); + let mut out = Vec::new(); + for (iface_name, session) in sessions.iter() { + if name.is_some_and(|want| want != iface_name) { + continue; + } + out.extend(session.table.lock().unwrap().handshakes()); + } + out.sort_by_key(|h| std::cmp::Reverse(h.last_seen_unix)); + out + } + + pub fn export_handshake( + &self, + name: &str, + bssid: &str, + station: &str, + out: impl AsRef, + ) -> Result { + let hs = self + .session_handshake(name, bssid, station)? + .ok_or_else(|| SabiaError::InvalidInput("handshake não encontrado".into()))?; + let frames = hs.raw_frames(); + if frames.is_empty() { + return Err(SabiaError::InvalidInput( + "handshake sem quadros crus (captura antiga)".into(), + )); + } + let path = out.as_ref(); + let mut writer = PcapWriter::create(path)?; + for frame in &frames { + writer.write_frame(frame)?; + } + writer.flush()?; + self.log( + "export_handshake", + name, + serde_json::json!({ "bssid": bssid, "station": station, "path": path, "frames": frames.len() }), + ); + Ok(ExportReport { + path: path.display().to_string(), + frames: frames.len(), + handshakes: 1, + }) + } + + pub fn export_all_handshakes(&self, name: &str, out: impl AsRef) -> Result { + let entries = { + let sessions = self.sessions.lock().unwrap(); + let session = sessions.get(name).ok_or_else(|| { + SabiaError::InvalidInput(format!("nenhuma sessão de captura em {name}")) + })?; + let entries = session.table.lock().unwrap().handshake_entries(); + entries + }; + let path = out.as_ref(); + let mut writer = PcapWriter::create(path)?; + let mut frames = 0usize; + let mut handshakes = 0usize; + for hs in &entries { + let raw = hs.raw_frames(); + if raw.is_empty() { + continue; + } + for frame in &raw { + writer.write_frame(frame)?; + frames += 1; + } + handshakes += 1; + } + writer.flush()?; + if handshakes == 0 { + return Err(SabiaError::InvalidInput( + "nenhum handshake com quadros crus para exportar".into(), + )); + } + self.log( + "export_handshakes", + name, + serde_json::json!({ "path": path, "frames": frames, "handshakes": handshakes }), + ); + Ok(ExportReport { + path: path.display().to_string(), + frames, + handshakes, + }) + } + + pub fn start_crack( + &self, + name: &str, + bssid: Option<&str>, + station: Option<&str>, + wordlist: impl AsRef, + ) -> Result { + let wordlist = wordlist.as_ref(); + if !wordlist.is_file() { + return Err(SabiaError::InvalidInput(format!( + "dicionário não encontrado: {}", + wordlist.display() + ))); + } + let table = { + let sessions = self.sessions.lock().unwrap(); + sessions.get(name).map(|s| s.table.clone()).ok_or_else(|| { + SabiaError::InvalidInput(format!("nenhuma sessão de captura em {name}")) + })? + }; + let hs = { + let table = table.lock().unwrap(); + match (bssid, station) { + (Some(b), Some(s)) => table.handshake(b, s).filter(|h| h.is_crackable()).cloned(), + (Some(b), None) => table.first_crackable(Some(b), None).cloned(), + (None, Some(s)) => table.first_crackable(None, Some(s)).cloned(), + (None, None) => table.first_crackable(None, None).cloned(), + } + } + .ok_or_else(|| { + SabiaError::InvalidInput("nenhum handshake quebrável para os filtros informados".into()) + })?; + + let id = format!("crack-{}", self.job_seq.fetch_add(1, Ordering::SeqCst) + 1); + let job = Arc::new(Mutex::new(CrackJobInfo { + id: id.clone(), + iface: name.to_string(), + bssid: Some(hs.bssid.clone()), + station: Some(hs.station.clone()), + status: "running".into(), + tried: 0, + started_unix: unix_now(), + finished_unix: None, + report: None, + error: None, + })); + self.jobs.lock().unwrap().insert(id.clone(), job.clone()); + + let audit_path = self.audit_path.clone(); + let audit_lock = self.audit_lock.clone(); + let iface_name = name.to_string(); + let wordlist_path = wordlist.to_path_buf(); + let job_thread = job.clone(); + let hs_bssid = hs.bssid.clone(); + let hs_station = hs.station.clone(); + let thread_wordlist = wordlist_path.clone(); + let thread_bssid = hs_bssid.clone(); + let thread_station = hs_station.clone(); + + std::thread::spawn(move || { + let progress_job = job_thread.clone(); + let result = crack::crack_handshake(&hs, &thread_wordlist, move |tried| { + if let Ok(mut j) = progress_job.lock() { + j.tried = tried; + } + }); + let mut guard = job_thread.lock().unwrap(); + guard.finished_unix = Some(unix_now()); + match result { + Ok(report) => { + guard.status = "done".into(); + guard.tried = report.tried; + if let Some(password) = report.password.as_deref() { + table.lock().unwrap().set_password( + &thread_bssid, + &thread_station, + password, + ); + append_audit( + &audit_path, + &audit_lock, + "crack_success", + &iface_name, + &serde_json::json!({ + "bssid": thread_bssid, + "station": thread_station, + "tried": report.tried, + "elapsed_ms": report.elapsed_ms, + }), + ); + } + guard.report = Some(report); + } + Err(e) => { + guard.status = "error".into(); + guard.error = Some(e.to_string()); } } }); - if let Ok(mut log) = AuditLog::open(&self.audit_path) { - let _ = log.record("scan_start", name, "{}"); + self.log( + "crack_start", + name, + serde_json::json!({ "job": id, "bssid": hs_bssid, "station": hs_station, "wordlist": wordlist_path }), + ); + Ok(id) + } + + pub fn crack_job(&self, id: &str) -> Option { + self.jobs + .lock() + .unwrap() + .get(id) + .map(|j| j.lock().unwrap().clone()) + } + + pub fn crack_jobs(&self) -> Vec { + let jobs = self.jobs.lock().unwrap(); + let mut out: Vec<_> = jobs.values().map(|j| j.lock().unwrap().clone()).collect(); + out.sort_by(|a, b| { + b.started_unix + .cmp(&a.started_unix) + .then_with(|| a.id.cmp(&b.id)) + }); + out + } + + #[cfg(target_os = "linux")] + pub fn deauth( + &self, + name: &str, + bssid: &str, + station: Option<&str>, + count: u32, + reason: u16, + ) -> Result { + use crate::inject::{self, RawInjector}; + + if !(1..=256).contains(&count) { + return Err(SabiaError::InvalidInput( + "quantidade de quadros deve ficar entre 1 e 256".into(), + )); } - sessions.insert(name.to_string(), Session { table, running }); - Ok(()) + let info = iface::get(name)?; + if info.mode == Mode::Managed { + return Err(SabiaError::InvalidInput(format!( + "{name} está em modo managed; ative o modo monitor antes de injetar" + ))); + } + let ap = parse_mac(bssid)?; + let dest = match station { + Some(s) => parse_mac(s)?, + None => inject::BROADCAST, + }; + let reason = inject::Reason::from_u16(reason); + let frame = inject::deauth_frame(ap, dest, reason); + let injector = RawInjector::open(name)?; + + let mut frames_sent = 0usize; + for _ in 0..count { + injector.send(&frame)?; + frames_sent += 1; + std::thread::sleep(Duration::from_millis(10)); + } + + let report = DeauthReport { + iface: name.to_string(), + bssid: bssid.to_string(), + station: station.unwrap_or("ff:ff:ff:ff:ff:ff").to_string(), + reason, + frames_sent, + }; + self.log( + "deauth", + name, + serde_json::json!({ + "bssid": bssid, + "station": report.station, + "reason": reason, + "frames": frames_sent, + }), + ); + Ok(report) } - /// Captura crua (AF_PACKET) só existe no Linux (ver `crate::capture`). - /// Em macOS/Windows o app compila e roda, mas a captura ainda não tem - /// backend nativo — próximo passo natural do projeto. #[cfg(not(target_os = "linux"))] - pub fn start_scan(&self, _name: &str) -> Result<()> { - Err(crate::error::SabiaError::UnsupportedPlatform) + pub fn deauth( + &self, + _name: &str, + _bssid: &str, + _station: Option<&str>, + _count: u32, + _reason: u16, + ) -> Result { + Err(SabiaError::UnsupportedPlatform) } - pub fn stop_scan(&self, name: &str) { - let mut sessions = self.sessions.lock().unwrap(); - if let Some(s) = sessions.remove(name) { - s.running.store(false, Ordering::SeqCst); - if let Ok(mut log) = AuditLog::open(&self.audit_path) { - let _ = log.record("scan_stop", name, "{}"); - } + pub fn audit_verify(&self) -> Result> { + if !self.audit_path.exists() { + return Ok(None); } + AuditLog::verify(&self.audit_path) } - pub fn scan_results(&self, name: &str) -> Vec { - let sessions = self.sessions.lock().unwrap(); - match sessions.get(name) { - Some(s) => s.table.lock().unwrap().access_points(), - None => Vec::new(), + pub fn audit_entries(&self, limit: Option) -> Result> { + if !self.audit_path.exists() { + return Ok(Vec::new()); } + AuditLog::read_entries(&self.audit_path, limit) } pub fn update_check(&self) -> Result { @@ -136,4 +739,178 @@ impl Engine { let exe = std::env::current_exe()?; update::apply(&exe) } + + fn session_handshake( + &self, + name: &str, + bssid: &str, + station: &str, + ) -> Result> { + let sessions = self.sessions.lock().unwrap(); + let session = sessions.get(name).ok_or_else(|| { + SabiaError::InvalidInput(format!("nenhuma sessão de captura em {name}")) + })?; + let table = session.table.lock().unwrap(); + Ok(table.handshake(bssid, station).cloned()) + } + + fn log(&self, action: &str, target: &str, details: serde_json::Value) { + append_audit(&self.audit_path, &self.audit_lock, action, target, &details); + } +} + +fn append_audit( + path: &Path, + lock: &Arc>, + action: &str, + target: &str, + details: &serde_json::Value, +) { + let _guard = lock.lock().unwrap(); + let Ok(mut log) = AuditLog::open(path) else { + return; + }; + let text = details.to_string(); + let _ = log.record(action, target, &text); +} + +fn parse_mac(text: &str) -> Result<[u8; 6]> { + crate::dot11::parse_mac(text) + .ok_or_else(|| SabiaError::InvalidInput(format!("MAC inválido: {text}"))) +} + +fn unix_now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::radiotap::Band; + use crate::testutil::{ + ap, beacon_frame, frame_with_eapol, handshake_frames, radiotap_wrap, sta, + }; + use tempfile::TempDir; + + fn seed_session(engine: &Engine, name: &str) -> Arc> { + let mut table = ScanTable::new(); + let beacon = radiotap_wrap(&beacon_frame(ap(), "RedeWPA", 6, &[2]), 2437, -40); + table.ingest(&beacon); + let (m1, m2) = handshake_frames("RedeWPA", "password", 2, false); + let f1 = frame_with_eapol(true, ap(), sta(), [0x33; 6], &m1); + let f2 = frame_with_eapol(false, ap(), sta(), [0x33; 6], &m2); + table.ingest(&radiotap_wrap(&f1, 2437, -40)); + table.ingest(&radiotap_wrap(&f2, 2437, -41)); + assert_eq!(table.stats().handshakes, 1); + + let table = Arc::new(Mutex::new(table)); + engine.sessions.lock().unwrap().insert( + name.to_string(), + Session { + table: table.clone(), + running: Arc::new(AtomicBool::new(false)), + channel: Arc::new(Mutex::new(Some(6))), + channels: vec![6], + started_unix: 1, + pcap_out: None, + error: Arc::new(Mutex::new(None)), + handle: None, + }, + ); + table + } + + #[test] + fn analyzes_pcap_and_snapshot() { + let dir = TempDir::new().unwrap(); + let pcap = dir.path().join("capture.pcap"); + let engine = Engine::with_audit_path(dir.path().join("audit.log")); + let table = seed_session(&engine, "wlan-test"); + let hs = { + let table = table.lock().unwrap(); + table + .handshake("00:11:22:33:44:55", "66:77:88:99:aa:bb") + .cloned() + .unwrap() + }; + + let mut writer = PcapWriter::create(&pcap).unwrap(); + let beacon = radiotap_wrap(&beacon_frame(ap(), "RedeWPA", 6, &[2]), 2437, -40); + writer.write_frame(&beacon).unwrap(); + for frame in hs.raw_frames() { + writer.write_frame(&frame).unwrap(); + } + writer.flush().unwrap(); + + let snapshot = engine.analyze_pcap(&pcap).unwrap(); + assert_eq!(snapshot.access_points.len(), 1); + assert_eq!(snapshot.access_points[0].ssid.as_deref(), Some("RedeWPA")); + assert_eq!(snapshot.access_points[0].band, Some(Band::B2G4)); + assert_eq!(snapshot.handshakes.len(), 1); + assert!(snapshot.handshakes[0].crackable); + assert_eq!(snapshot.stats.frames, 3); + } + + #[test] + fn exports_handshake_and_cracks_it() { + let dir = TempDir::new().unwrap(); + let engine = Engine::with_audit_path(dir.path().join("audit.log")); + seed_session(&engine, "wlan-test"); + + let export = dir.path().join("hs.pcap"); + let report = engine + .export_handshake( + "wlan-test", + "00:11:22:33:44:55", + "66:77:88:99:aa:bb", + &export, + ) + .unwrap(); + assert_eq!(report.frames, 2); + assert!(export.is_file()); + + let wordlist = dir.path().join("words.txt"); + std::fs::write(&wordlist, "errada\noutra\npassword\nnunca\n").unwrap(); + let id = engine + .start_crack( + "wlan-test", + Some("00:11:22:33:44:55"), + Some("66:77:88:99:aa:bb"), + &wordlist, + ) + .unwrap(); + + let deadline = Instant::now() + Duration::from_secs(10); + loop { + let job = engine.crack_job(&id).unwrap(); + if job.status != "running" { + assert_eq!(job.status, "done"); + assert_eq!( + job.report.as_ref().and_then(|r| r.password.as_deref()), + Some("password") + ); + break; + } + assert!(Instant::now() < deadline, "job não terminou"); + std::thread::sleep(Duration::from_millis(20)); + } + + let hs = engine.handshakes(Some("wlan-test")); + assert_eq!(hs[0].password.as_deref(), Some("password")); + } + + #[test] + fn errors_are_clear_for_missing_inputs() { + let dir = TempDir::new().unwrap(); + let engine = Engine::with_audit_path(dir.path().join("audit.log")); + assert!(engine.scan_snapshot("nope").is_err()); + assert!(engine + .start_crack("nope", None, None, dir.path().join("nada.txt")) + .is_err()); + assert!(engine.crack_job("nope").is_none()); + assert!(engine.audit_entries(Some(10)).unwrap().is_empty()); + } } diff --git a/crates/sabia-core/src/error.rs b/crates/sabia-core/src/error.rs index 9b20a26..c38a2b7 100644 --- a/crates/sabia-core/src/error.rs +++ b/crates/sabia-core/src/error.rs @@ -17,6 +17,12 @@ pub enum SabiaError { #[error("captura crua só é suportada no Linux (AF_PACKET)")] UnsupportedPlatform, + #[error("entrada inválida: {0}")] + InvalidInput(String), + + #[error("captura .pcap inválida: {0}")] + InvalidPcap(String), + #[error("erro de E/S: {0}")] Io(#[from] io::Error), diff --git a/crates/sabia-core/src/handshake.rs b/crates/sabia-core/src/handshake.rs new file mode 100644 index 0000000..8a10ab0 --- /dev/null +++ b/crates/sabia-core/src/handshake.rs @@ -0,0 +1,369 @@ +//! Montagem dos handshakes WPA/WPA2 a partir dos quadros EAPOL vistos no ar. +//! +//! Um handshake é identificado pelo par (BSSID, estação). Guardamos as quatro +//! mensagens quando aparecem, o PMKID da mensagem 1 e os quadros crus +//! necessários para exportar um `.pcap` que o `aircrack-ng`/hashcat também +//! entendam. + +use crate::dot11; +use crate::eapol::{self, EapolKey}; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct RawFrame { + pub message: u8, + pub raw: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct Handshake { + pub bssid: String, + pub station: String, + pub ssid: Option, + pub anonce: Option<[u8; 32]>, + pub snonce: Option<[u8; 32]>, + pub pmkid: Option<[u8; 16]>, + pub key_version: u8, + /// Bitmask das mensagens vistas (bit 0 = mensagem 1). + pub messages: u8, + /// Quadro EAPOL da mensagem 2, com o MIC — necessário para conferir a + /// senha candidata. + pub eapol_msg2: Option>, + pub raw: Vec, + pub first_seen_unix: u64, + pub last_seen_unix: u64, + pub password: Option, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct HandshakeInfo { + pub bssid: String, + pub station: String, + pub ssid: Option, + pub kind: String, + pub messages: String, + pub complete: bool, + pub crackable: bool, + pub pmkid: Option, + pub key_version: u8, + pub password: Option, + pub raw_frames: usize, + pub first_seen_unix: u64, + pub last_seen_unix: u64, +} + +impl Handshake { + pub fn message_list(&self) -> String { + let mut seen = Vec::new(); + for msg in 1..=4 { + if self.messages & (1 << (msg - 1)) != 0 { + seen.push(msg.to_string()); + } + } + seen.join(",") + } + + pub fn kind(&self) -> &'static str { + match (self.pmkid.is_some(), self.eapol_msg2.is_some()) { + (true, true) => "EAPOL + PMKID", + (true, false) => "PMKID", + (false, _) => "EAPOL", + } + } + + pub fn is_complete(&self) -> bool { + self.pmkid.is_some() + || (self.anonce.is_some() && self.snonce.is_some() && self.eapol_msg2.is_some()) + } + + pub fn is_crackable(&self) -> bool { + let ssid_ok = self.ssid.as_ref().is_some_and(|s| !s.is_empty()); + if !ssid_ok { + return false; + } + if self.pmkid.is_some() { + return true; + } + self.anonce.is_some() && self.eapol_msg2.is_some() && matches!(self.key_version, 1 | 2) + } + + pub fn info(&self) -> HandshakeInfo { + HandshakeInfo { + bssid: self.bssid.clone(), + station: self.station.clone(), + ssid: self.ssid.clone(), + kind: self.kind().to_string(), + messages: self.message_list(), + complete: self.is_complete(), + crackable: self.is_crackable(), + pmkid: self.pmkid.map(|p| hex(&p)), + key_version: self.key_version, + password: self.password.clone(), + raw_frames: self.raw.len(), + first_seen_unix: self.first_seen_unix, + last_seen_unix: self.last_seen_unix, + } + } + + /// Quadros crus (radiotap + 802.11) das mensagens vistas, em ordem. + pub fn raw_frames(&self) -> Vec> { + let mut v = self.raw.clone(); + v.sort_by_key(|f| f.message); + v.into_iter().map(|f| f.raw).collect() + } + + fn store_raw(&mut self, message: u8, raw: &[u8]) { + if let Some(existing) = self.raw.iter_mut().find(|f| f.message == message) { + existing.raw = raw.to_vec(); + } else { + self.raw.push(RawFrame { + message, + raw: raw.to_vec(), + }); + } + } +} + +#[derive(Debug, Default)] +pub struct HandshakeTracker { + map: HashMap<(String, String), Handshake>, +} + +impl HandshakeTracker { + pub fn new() -> Self { + Self::default() + } + + /// Processa um quadro 802.11. Retorna `true` quando o quadro era EAPOL e + /// atualizou (ou criou) um handshake. + pub fn ingest( + &mut self, + frame: &dot11::Frame<'_>, + raw: &[u8], + ssid_hint: Option<&str>, + now: u64, + ) -> bool { + let Some(payload) = frame.eapol() else { + return false; + }; + let Some(key) = eapol::parse(payload) else { + return false; + }; + if key.message == 0 || !key.pairwise { + return false; + } + + // Em data frames com DS, o BSSID está em `frame.bssid`; a estação é + // quem recebe (1/3, AP -> STA) ou quem transmite (2/4, STA -> AP). + let bssid = frame.bssid_str(); + let station = match key.message { + 1 | 3 => frame.addr1.map(|m| dot11::mac_to_string(&m)), + _ => frame.addr2.map(|m| dot11::mac_to_string(&m)), + }; + let (Some(bssid), Some(station)) = (bssid, station) else { + return false; + }; + if bssid == station { + return false; + } + + let entry = self + .map + .entry((bssid.clone(), station.clone())) + .or_insert_with(|| Handshake { + bssid, + station, + first_seen_unix: now, + ..Default::default() + }); + if entry.ssid.is_none() { + if let Some(ssid) = ssid_hint.filter(|s| !s.is_empty()) { + entry.ssid = Some(ssid.to_string()); + } + } + if entry.first_seen_unix == 0 { + entry.first_seen_unix = now; + } + entry.last_seen_unix = now; + entry.messages |= 1 << (key.message - 1); + entry.key_version = key.key_version; + entry.store_raw(key.message, raw); + apply_key(entry, &key); + true + } + + /// Atualiza o SSID de handshakes já vistos (quando o beacon chega depois + /// do EAPOL, o que acontece em capturas começando no meio do handshake). + pub fn update_ssid(&mut self, bssid: &str, ssid: &str) { + if ssid.is_empty() { + return; + } + for hs in self.map.values_mut() { + if hs.bssid == bssid && hs.ssid.is_none() { + hs.ssid = Some(ssid.to_string()); + } + } + } + + pub fn get(&self, bssid: &str, station: &str) -> Option<&Handshake> { + self.map.get(&(bssid.to_string(), station.to_string())) + } + + pub fn get_mut(&mut self, bssid: &str, station: &str) -> Option<&mut Handshake> { + self.map.get_mut(&(bssid.to_string(), station.to_string())) + } + + pub fn all(&self) -> Vec<&Handshake> { + let mut v: Vec<_> = self.map.values().collect(); + v.sort_by(|a, b| { + b.last_seen_unix + .cmp(&a.last_seen_unix) + .then_with(|| a.bssid.cmp(&b.bssid)) + }); + v + } + + pub fn infos(&self) -> Vec { + self.all().into_iter().map(Handshake::info).collect() + } + + /// Primeiro handshake que pode ser quebrado; filtros opcionais por BSSID + /// e estação. + pub fn first_crackable( + &self, + bssid: Option<&str>, + station: Option<&str>, + ) -> Option<&Handshake> { + self.all().into_iter().find(|h| { + h.is_crackable() + && bssid.map_or(true, |b| b.eq_ignore_ascii_case(&h.bssid)) + && station.map_or(true, |s| s.eq_ignore_ascii_case(&h.station)) + }) + } + + pub fn set_password(&mut self, bssid: &str, station: &str, password: &str) { + if let Some(hs) = self.get_mut(bssid, station) { + hs.password = Some(password.to_string()); + } + } + + pub fn len(&self) -> usize { + self.map.len() + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } +} + +fn apply_key(hs: &mut Handshake, key: &EapolKey) { + match key.message { + 1 => { + hs.anonce = Some(key.nonce); + if key.pmkid.is_some() { + hs.pmkid = key.pmkid; + } + } + 2 => { + hs.snonce = Some(key.nonce); + hs.eapol_msg2 = Some(key.frame.clone()); + } + 3 => hs.anonce = Some(key.nonce), + 4 => hs.snonce = Some(key.nonce), + _ => {} + } +} + +fn hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + use std::fmt::Write; + let _ = write!(s, "{b:02x}"); + } + s +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::eapol::KEY_DATA_OFFSET; + use crate::testutil::{frame_with_eapol, key_frame}; + + fn key_frame_for(message: u8, nonce: [u8; 32], key_data: &[u8]) -> Vec { + key_frame(message, 2, nonce, key_data) + } + + fn ap() -> [u8; 6] { + [0x11; 6] + } + fn sta() -> [u8; 6] { + [0x22; 6] + } + + #[test] + fn assembles_complete_handshake() { + let mut tracker = HandshakeTracker::new(); + let m1 = key_frame_for(1, [0xaa; 32], &[]); + let f1 = frame_with_eapol(true, ap(), sta(), [0x33; 6], &m1); + let parsed1 = dot11::parse(&f1).expect("frame"); + assert!(tracker.ingest(&parsed1, &f1, Some("MinhaRede"), 10)); + + let m2 = key_frame_for(2, [0xbb; 32], &[]); + let f2 = frame_with_eapol(false, ap(), sta(), [0x33; 6], &m2); + let parsed2 = dot11::parse(&f2).expect("frame"); + assert!(tracker.ingest(&parsed2, &f2, Some("MinhaRede"), 11)); + + let hs = tracker + .get("11:11:11:11:11:11", "22:22:22:22:22:22") + .expect("hs"); + assert_eq!(hs.ssid.as_deref(), Some("MinhaRede")); + assert_eq!(hs.messages, 0b11); + assert!(hs.is_complete()); + assert!(hs.is_crackable()); + assert_eq!(hs.anonce, Some([0xaa; 32])); + assert_eq!(hs.snonce, Some([0xbb; 32])); + assert_eq!(hs.eapol_msg2.as_deref(), Some(&m2[..])); + assert_eq!(hs.raw_frames().len(), 2); + } + + #[test] + fn captures_pmkid_from_message_one() { + let mut data = vec![0xdd, 20, 0x00, 0x0f, 0xac, 0x04]; + data.extend_from_slice(&[0xcd; 16]); + let m1 = key_frame_for(1, [0xaa; 32], &data); + assert_eq!(m1.len(), KEY_DATA_OFFSET + data.len()); + let f1 = frame_with_eapol(true, ap(), sta(), [0x33; 6], &m1); + let mut tracker = HandshakeTracker::new(); + let parsed = dot11::parse(&f1).expect("frame"); + assert!(tracker.ingest(&parsed, &f1, Some("Rede"), 1)); + let hs = tracker + .get("11:11:11:11:11:11", "22:22:22:22:22:22") + .expect("hs"); + assert_eq!(hs.pmkid, Some([0xcd; 16])); + assert!(hs.is_complete()); + assert!(hs.is_crackable()); + assert_eq!(hs.kind(), "PMKID"); + } + + #[test] + fn late_beacon_fills_ssid() { + let m1 = key_frame_for(1, [1; 32], &[]); + let f1 = frame_with_eapol(true, ap(), sta(), [0x33; 6], &m1); + let mut tracker = HandshakeTracker::new(); + let parsed = dot11::parse(&f1).expect("frame"); + tracker.ingest(&parsed, &f1, None, 1); + assert!(!tracker + .get("11:11:11:11:11:11", "22:22:22:22:22:22") + .unwrap() + .is_crackable()); + tracker.update_ssid("11:11:11:11:11:11", "RedeTardia"); + assert!( + tracker + .get("11:11:11:11:11:11", "22:22:22:22:22:22") + .unwrap() + .ssid + .as_deref() + == Some("RedeTardia") + ); + } +} diff --git a/crates/sabia-core/src/inject.rs b/crates/sabia-core/src/inject.rs new file mode 100644 index 0000000..3a6989c --- /dev/null +++ b/crates/sabia-core/src/inject.rs @@ -0,0 +1,164 @@ +//! Construção e envio de quadros de gerenciamento (desautenticação e +//! desassociação) para testes autorizados de robustez de redes Wi-Fi. +//! +//! A construção do quadro é independente de plataforma e testável; o envio +//! usa `AF_PACKET` e só existe no Linux. O chamador (engine) impõe limites de +//! quantidade e intervalo e registra a ação na trilha de auditoria. + +use crate::error::Result; +#[cfg(target_os = "linux")] +use crate::error::SabiaError; + +pub const BROADCAST: [u8; 6] = [0xff; 6]; + +/// Cabeçalho radiotap mínimo (8 bytes, sem campos). É aceito por mac80211 e +/// deixa o driver escolher a taxa; suficiente para quadros de gerenciamento. +fn radiotap_min() -> [u8; 8] { + [0, 0, 8, 0, 0, 0, 0, 0] +} + +fn mgmt_frame(subtype: u8, bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec { + let fc: u16 = (subtype as u16) << 4; + let mut f = Vec::with_capacity(8 + 24 + 2); + f.extend_from_slice(&radiotap_min()); + f.extend_from_slice(&fc.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&destination); // addr1: destino + f.extend_from_slice(&bssid); // addr2: BSSID/transmissor + f.extend_from_slice(&bssid); // addr3: BSSID + f.extend_from_slice(&[0u8; 2]); // número de sequência + f.extend_from_slice(&reason.to_le_bytes()); + f +} + +/// Quadro de desautenticação (subtipo 12). `destination` pode ser uma +/// estação específica ou [`BROADCAST`]. +pub fn deauth_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec { + mgmt_frame(12, bssid, destination, reason) +} + +/// Quadro de desassociação (subtipo 10). +pub fn disassoc_frame(bssid: [u8; 6], destination: [u8; 6], reason: u16) -> Vec { + mgmt_frame(10, bssid, destination, reason) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Reason { + Unspecified = 1, + PreviousAuthInvalid = 2, + DeauthLeaving = 3, + Inactivity = 4, + Class2FrameFromNonAuth = 7, +} + +impl Reason { + pub fn from_u16(value: u16) -> u16 { + if value == 0 { + 1 + } else { + value + } + } +} + +#[cfg(target_os = "linux")] +pub struct RawInjector { + fd: std::os::fd::RawFd, +} + +#[cfg(target_os = "linux")] +impl RawInjector { + pub fn open(iface: &str) -> Result { + use std::ffi::CString; + use std::io; + use std::mem; + + let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, 0x0003) }; + if fd < 0 { + let err = io::Error::last_os_error(); + return Err(if err.raw_os_error() == Some(libc::EPERM) { + SabiaError::PermissionDenied + } else { + SabiaError::Io(err) + }); + } + let cname = + CString::new(iface).map_err(|_| SabiaError::InterfaceNotFound(iface.to_string()))?; + let ifindex = unsafe { libc::if_nametoindex(cname.as_ptr()) }; + if ifindex == 0 { + unsafe { libc::close(fd) }; + return Err(SabiaError::InterfaceNotFound(iface.to_string())); + } + let mut sll: libc::sockaddr_ll = unsafe { mem::zeroed() }; + sll.sll_family = libc::AF_PACKET as u16; + sll.sll_protocol = 0x0003u16.to_be(); + sll.sll_ifindex = ifindex as i32; + let ret = unsafe { + libc::bind( + fd, + &sll as *const libc::sockaddr_ll as *const libc::sockaddr, + mem::size_of::() as u32, + ) + }; + if ret < 0 { + let err = io::Error::last_os_error(); + unsafe { libc::close(fd) }; + return Err(SabiaError::Io(err)); + } + Ok(Self { fd }) + } + + pub fn send(&self, frame: &[u8]) -> Result { + let n = unsafe { + libc::send( + self.fd, + frame.as_ptr() as *const libc::c_void, + frame.len(), + 0, + ) + }; + if n < 0 { + return Err(SabiaError::Io(std::io::Error::last_os_error())); + } + Ok(n as usize) + } +} + +#[cfg(target_os = "linux")] +impl Drop for RawInjector { + fn drop(&mut self) { + unsafe { libc::close(self.fd) }; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn bssid() -> [u8; 6] { + [0x00, 0x11, 0x22, 0x33, 0x44, 0x55] + } + + fn station() -> [u8; 6] { + [0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb] + } + + #[test] + fn builds_deauth_frame() { + let f = deauth_frame(bssid(), station(), 7); + assert_eq!(&f[0..4], &[0, 0, 8, 0]); + assert_eq!(u16::from_le_bytes([f[8], f[9]]), 0x00c0); + assert_eq!(&f[12..18], &station()); + assert_eq!(&f[18..24], &bssid()); + assert_eq!(&f[24..30], &bssid()); + assert_eq!(u16::from_le_bytes([f[32], f[33]]), 7); + } + + #[test] + fn builds_disassoc_broadcast_frame() { + let f = disassoc_frame(bssid(), BROADCAST, 1); + assert_eq!(u16::from_le_bytes([f[8], f[9]]), 0x00a0); + assert_eq!(&f[12..18], &BROADCAST); + assert_eq!(f.len(), 8 + 24 + 2); + } +} diff --git a/crates/sabia-core/src/lib.rs b/crates/sabia-core/src/lib.rs index 179c31f..c5cbc62 100644 --- a/crates/sabia-core/src/lib.rs +++ b/crates/sabia-core/src/lib.rs @@ -1,14 +1,20 @@ -//! `sabia-core`: tudo que fala com o rádio Wi-Fi, interpreta quadros 802.11 -//! e mantém a trilha de auditoria. A CLI e a interface gráfica são só -//! consumidoras disto — a parte complexa (netlink, radiotap, parsing de -//! frame) fica inteira aqui dentro. +//! `sabia-core`: tudo que fala com o rádio Wi-Fi, interpreta quadros 802.11, +//! mantém a trilha de auditoria e quebra handshakes. A CLI e a interface +//! gráfica são só consumidoras disto — a parte complexa (radiotap, 802.11, +//! EAPOL, captura e injeção) fica inteira aqui dentro. pub mod audit; +pub mod crack; +pub mod doctor; pub mod dot11; +pub mod eapol; pub mod engine; pub mod error; +pub mod handshake; pub mod iface; +pub mod inject; pub mod netlink; +pub mod pcapreader; pub mod pcapwriter; pub mod radiotap; pub mod scan; @@ -17,4 +23,7 @@ pub mod update; #[cfg(target_os = "linux")] pub mod capture; +#[cfg(test)] +pub(crate) mod testutil; + pub use error::{Result, SabiaError}; diff --git a/crates/sabia-core/src/netlink.rs b/crates/sabia-core/src/netlink.rs index 2561ee4..5bbb415 100644 --- a/crates/sabia-core/src/netlink.rs +++ b/crates/sabia-core/src/netlink.rs @@ -1,15 +1,26 @@ -//! Controle de modo (managed/monitor) e canal via `iw`. +//! Controle de modo (managed/monitor), canal e leitura das capacidades do +//! rádio via `iw`. //! -//! Isso é uma ponte deliberada: o protocolo real é nl80211 por netlink, e -//! trocar este módulo por uma implementação nativa (sem depender do binário -//! `iw` instalado) é o próximo passo natural. Por ora, `iw` é amplamente -//! disponível, testado e evita reimplementar genl/nl80211 antes de validar -//! o resto do pipeline com o adaptador real. +//! É uma ponte deliberada: o protocolo real é nl80211 por netlink, e trocar +//! este módulo por uma implementação nativa (sem depender dos binários `iw` e +//! `ip`) é o próximo passo natural. Por ora `iw`/`ip` são universais em +//! Linux, testados, e evitam reimplementar genl/nl80211 antes de validar o +//! pipeline com hardware real. use crate::error::{Result, SabiaError}; use crate::iface::Mode; +use crate::radiotap::{band_for_freq, Band}; use std::process::Command; +#[derive(Debug, Clone, serde::Serialize)] +pub struct ChannelInfo { + pub channel: u32, + pub freq_mhz: u16, + pub band: Band, + pub disabled: bool, + pub max_power_dbm: Option, +} + fn run(cmd: &str, args: &[&str]) -> Result { let out = Command::new(cmd) .args(args) @@ -25,7 +36,10 @@ fn run(cmd: &str, args: &[&str]) -> Result { } else { detail }; - if detail.contains("Operation not permitted") || detail.contains("nl80211 not found") { + if detail.contains("Operation not permitted") + || detail.contains("Insufficient privileges") + || detail.contains("nl80211 not found") + { return Err(SabiaError::PermissionDenied); } return Err(SabiaError::CommandFailed { @@ -36,6 +50,28 @@ fn run(cmd: &str, args: &[&str]) -> Result { Ok(String::from_utf8_lossy(&out.stdout).into_owned()) } +pub fn command_exists(name: &str) -> bool { + std::env::var_os("PATH") + .map(|paths| { + std::env::split_paths(&paths).any(|dir| { + let p = dir.join(name); + p.is_file() + }) + }) + .unwrap_or(false) +} + +pub fn is_root() -> bool { + #[cfg(unix)] + { + unsafe { libc::geteuid() == 0 } + } + #[cfg(not(unix))] + { + false + } +} + pub fn iw_dev_type(iface: &str) -> Option { let out = run("iw", &["dev", iface, "info"]).ok()?; for line in out.lines() { @@ -51,35 +87,76 @@ pub fn iw_dev_type(iface: &str) -> Option { None } -/// Lista (canal, frequência) suportados pelo phy da interface. -pub fn supported_channels(iface: &str) -> Result> { +pub fn phy_for_iface(iface: &str) -> Result { let out = run("iw", &["dev", iface, "info"])?; - let phy = out - .lines() + out.lines() .find_map(|l| l.trim().strip_prefix("wiphy ")) .map(|n| format!("phy{n}")) .ok_or_else(|| SabiaError::CommandFailed { cmd: "iw dev info".into(), detail: "wiphy não encontrado na saída".into(), - })?; + }) +} + +/// Lista canais suportados pelo phy da interface. +pub fn channels(iface: &str) -> Result> { + let phy = phy_for_iface(iface)?; let info = run("iw", &["phy", &phy, "info"])?; - let mut channels = Vec::new(); - for line in info.lines() { - let line = line.trim(); - // formato: "* 2412 MHz [1] (20.0 dBm)" - if let Some(rest) = line.strip_prefix("* ") { - if let Some(start) = rest.find('[') { - if let Some(end) = rest[start..].find(']') { - if let Ok(ch) = rest[start + 1..start + end].parse::() { - channels.push(ch); - } - } + Ok(parse_phy_channels(&info)) +} + +pub fn supported_channels(iface: &str) -> Result> { + Ok(channels(iface)? + .into_iter() + .filter(|c| !c.disabled) + .map(|c| c.channel) + .collect()) +} + +/// Canais a percorrer. `band` aceita `"2.4"`, `"5"`, `"6"`, `"all"`/`None`. +pub fn channel_plan(iface: &str, band: Option<&str>) -> Result> { + let want = match band.map(|b| b.trim().to_ascii_lowercase()) { + None => None, + Some(b) if b.is_empty() || b == "all" || b == "auto" => None, + Some(b) => Some(match b.as_str() { + "2.4" | "2.4ghz" | "2g" => Band::B2G4, + "5" | "5ghz" | "5g" => Band::B5, + "6" | "6ghz" | "6g" => Band::B6, + other => { + return Err(SabiaError::InvalidInput(format!( + "banda `{other}` inválida (use 2.4, 5, 6 ou all)" + ))) } - } - } - channels.sort_unstable(); - channels.dedup(); - Ok(channels) + }), + }; + let mut v: Vec = channels(iface)? + .into_iter() + .filter(|c| !c.disabled) + .filter(|c| want.map_or(true, |w| c.band == w)) + .map(|c| c.channel) + .collect(); + v.sort_unstable(); + v.dedup(); + Ok(v) +} + +pub fn monitor_supported(iface: &str) -> bool { + let Ok(phy) = phy_for_iface(iface) else { + return false; + }; + let Ok(info) = run("iw", &["phy", &phy, "info"]) else { + return false; + }; + info.lines().any(|l| l.trim() == "* monitor") +} + +pub fn driver_name(iface: &str) -> Option { + let path = std::path::Path::new("/sys/class/net") + .join(iface) + .join("device/driver"); + std::fs::read_link(path) + .ok() + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned())) } /// Põe a interface em modo monitor. Requer `CAP_NET_ADMIN` (normalmente root). @@ -99,3 +176,77 @@ pub fn set_channel(iface: &str, channel: u32) -> Result<()> { )?; Ok(()) } + +pub fn parse_phy_channels(text: &str) -> Vec { + let mut out = Vec::new(); + for line in text.lines() { + let line = line.trim(); + let Some(rest) = line.strip_prefix("* ") else { + continue; + }; + let Some(mhz_at) = rest.find(" MHz") else { + continue; + }; + let Ok(freq) = rest[..mhz_at].trim().parse::() else { + continue; + }; + let Some(open) = rest.find('[') else { + continue; + }; + let Some(close_rel) = rest[open..].find(']') else { + continue; + }; + let Ok(channel) = rest[open + 1..open + close_rel].parse::() else { + continue; + }; + let freq = freq.round() as u16; + let disabled = rest.contains("(disabled)") || rest.contains("no IR"); + let max_power_dbm = rest + .rfind('(') + .and_then(|p| rest[p + 1..].strip_suffix(" dBm)")) + .and_then(|v| v.trim().parse::().ok()); + out.push(ChannelInfo { + channel, + freq_mhz: freq, + band: band_for_freq(freq), + disabled, + max_power_dbm, + }); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &str = "\ +Wiphy phy0 +\tBand 1: +\t\tFrequencies: +\t\t\t* 2412.0 MHz [1] (20.0 dBm) +\t\t\t* 2437.0 MHz [6] (20.0 dBm) +\t\t\t* 2484.0 MHz [14] (disabled) +\tBand 2: +\t\tFrequencies: +\t\t\t* 5180.0 MHz [36] (23.0 dBm) +\t\t\t* 5200.0 MHz [40] (23.0 dBm) +\tBand 4: +\t\tFrequencies: +\t\t\t* 5955.0 MHz [1] (12.0 dBm) +"; + + #[test] + fn parses_iw_phy_output() { + let ch = parse_phy_channels(SAMPLE); + assert_eq!(ch.len(), 6); + assert_eq!(ch[0].channel, 1); + assert_eq!(ch[0].freq_mhz, 2412); + assert_eq!(ch[0].band, Band::B2G4); + assert_eq!(ch[0].max_power_dbm, Some(20.0)); + assert!(ch[2].disabled); + assert_eq!(ch[3].band, Band::B5); + assert_eq!(ch[5].band, Band::B6); + assert_eq!(ch[5].channel, 1); + } +} diff --git a/crates/sabia-core/src/pcapreader.rs b/crates/sabia-core/src/pcapreader.rs new file mode 100644 index 0000000..79abe74 --- /dev/null +++ b/crates/sabia-core/src/pcapreader.rs @@ -0,0 +1,156 @@ +//! Leitor do formato `.pcap` clássico, usado para analisar capturas feitas +//! pelo próprio Sabiá (link-type 127, radiotap) e pelo aircrack-ng/tcpdump +//! (link-type 105, 802.11 cru). + +use crate::error::{Result, SabiaError}; +use std::fs::File; +use std::io::{BufReader, Read}; +use std::path::Path; + +pub const LINKTYPE_ETHERNET: u32 = 1; +pub const LINKTYPE_IEEE802_11: u32 = 105; +pub const LINKTYPE_IEEE802_11_RADIOTAP: u32 = 127; + +#[derive(Debug, Clone)] +pub struct PcapFrame { + pub data: Vec, + pub ts_sec: u32, + pub ts_usec: u32, +} + +pub struct PcapReader { + input: BufReader, + little_endian: bool, + nanosecond: bool, + linktype: u32, +} + +impl PcapReader { + pub fn open(path: impl AsRef) -> Result { + let file = File::open(path)?; + let mut input = BufReader::new(file); + let mut header = [0u8; 24]; + input + .read_exact(&mut header) + .map_err(|e| SabiaError::InvalidPcap(format!("cabeçalho incompleto: {e}")))?; + + let magic_le = u32::from_le_bytes(header[0..4].try_into().unwrap()); + let (little_endian, nanosecond) = match magic_le { + 0xa1b2_c3d4 => (true, false), + 0xa1b2_3c4d => (true, true), + 0xd4c3_b2a1 => (false, false), + 0x4d3c_b2a1 => (false, true), + other => { + return Err(SabiaError::InvalidPcap(format!( + "magic 0x{other:08x} não reconhecido" + ))) + } + }; + + let read_u32 = |bytes: &[u8]| { + let arr: [u8; 4] = bytes.try_into().unwrap(); + if little_endian { + u32::from_le_bytes(arr) + } else { + u32::from_be_bytes(arr) + } + }; + let snaplen = read_u32(&header[16..20]); + let linktype = read_u32(&header[20..24]); + if snaplen == 0 || snaplen > 16 * 1024 * 1024 { + return Err(SabiaError::InvalidPcap(format!( + "snaplen inválido: {snaplen}" + ))); + } + + Ok(Self { + input, + little_endian, + nanosecond, + linktype, + }) + } + + pub fn linktype(&self) -> u32 { + self.linktype + } + + /// Próximo quadro; `None` no fim do arquivo. + pub fn next_frame(&mut self) -> Result> { + let mut rec = [0u8; 16]; + match self.input.read_exact(&mut rec) { + Ok(()) => {} + Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None), + Err(e) => return Err(e.into()), + } + let read_u32 = |bytes: &[u8]| { + let arr: [u8; 4] = bytes.try_into().unwrap(); + if self.little_endian { + u32::from_le_bytes(arr) + } else { + u32::from_be_bytes(arr) + } + }; + let ts_sec = read_u32(&rec[0..4]); + let ts_usec = read_u32(&rec[4..8]); + let caplen = read_u32(&rec[8..12]) as usize; + if caplen > 16 * 1024 * 1024 { + return Err(SabiaError::InvalidPcap(format!("caplen absurdo: {caplen}"))); + } + let mut data = vec![0u8; caplen]; + self.input + .read_exact(&mut data) + .map_err(|e| SabiaError::InvalidPcap(format!("quadro truncado: {e}")))?; + + // Capturas sem radiotap (link-type 105) recebem um cabeçalho mínimo + // para o mesmo pipeline de parsing funcionar. + if self.linktype == LINKTYPE_IEEE802_11 { + let mut with_radiotap = vec![0u8, 0, 8, 0, 0, 0, 0, 0]; + with_radiotap.extend_from_slice(&data); + data = with_radiotap; + } + + let ts_usec = if self.nanosecond { + ts_usec / 1000 + } else { + ts_usec + }; + Ok(Some(PcapFrame { + data, + ts_sec, + ts_usec, + })) + } + + pub fn supports_80211(&self) -> bool { + matches!( + self.linktype, + LINKTYPE_IEEE802_11 | LINKTYPE_IEEE802_11_RADIOTAP + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pcapwriter::PcapWriter; + use tempfile::NamedTempFile; + + #[test] + fn roundtrips_frames_written_by_sabia() { + let file = NamedTempFile::new().unwrap(); + { + let mut w = PcapWriter::create(file.path()).unwrap(); + w.write_frame(&[0, 0, 8, 0, 0, 0, 0, 0, 0x80, 0]).unwrap(); + w.write_frame(&[1, 2, 3, 4]).unwrap(); + w.flush().unwrap(); + } + let mut r = PcapReader::open(file.path()).unwrap(); + assert_eq!(r.linktype(), LINKTYPE_IEEE802_11_RADIOTAP); + let f1 = r.next_frame().unwrap().unwrap(); + assert_eq!(f1.data.len(), 10); + let f2 = r.next_frame().unwrap().unwrap(); + assert_eq!(f2.data, vec![1, 2, 3, 4]); + assert!(r.next_frame().unwrap().is_none()); + } +} diff --git a/crates/sabia-core/src/radiotap.rs b/crates/sabia-core/src/radiotap.rs index da37b67..ec8fbb3 100644 --- a/crates/sabia-core/src/radiotap.rs +++ b/crates/sabia-core/src/radiotap.rs @@ -1,19 +1,52 @@ -//! Parser mínimo de cabeçalho Radiotap — só os campos que usamos -//! (canal/frequência e força de sinal). Especificação: -//! +//! Parser de cabeçalho Radiotap. +//! +//! Só extraímos o que o restante do núcleo usa — frequência/canal e força do +//! sinal — mas avançamos pelos campos na ordem da especificação, respeitando +//! alinhamento, para não depender do formato exato que cada driver emite. +//! Especificação: + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)] +pub enum Band { + #[serde(rename = "2.4 GHz")] + B2G4, + #[serde(rename = "5 GHz")] + B5, + #[serde(rename = "6 GHz")] + B6, + #[serde(rename = "desconhecida")] + Unknown, +} + +impl Band { + pub fn label(self) -> &'static str { + match self { + Band::B2G4 => "2.4 GHz", + Band::B5 => "5 GHz", + Band::B6 => "6 GHz", + Band::Unknown => "desconhecida", + } + } +} #[derive(Debug, Clone, Copy, Default)] pub struct RadiotapInfo { pub freq_mhz: Option, + pub channel: Option, + pub band: Option, pub signal_dbm: Option, + pub noise_dbm: Option, pub header_len: usize, + /// Bitmap "present" da primeira palavra (campos padrão). + pub present: u32, } -// Índices de bit no bitmap "present" que nos interessam. +const BIT_TSFT: u32 = 1 << 0; const BIT_FLAGS: u32 = 1 << 1; const BIT_RATE: u32 = 1 << 2; const BIT_CHANNEL: u32 = 1 << 3; +const BIT_FHSS: u32 = 1 << 4; const BIT_ANTSIGNAL: u32 = 1 << 5; +const BIT_ANTNOISE: u32 = 1 << 6; /// Retorna as infos extraídas e quantos bytes o cabeçalho ocupa (para o /// chamador saber onde o frame 802.11 propriamente começa). @@ -22,15 +55,18 @@ pub fn parse(buf: &[u8]) -> Option { return None; // version deve ser 0 } let header_len = u16::from_le_bytes([buf[2], buf[3]]) as usize; - if header_len > buf.len() || header_len < 8 { + if header_len < 8 || header_len > buf.len() { return None; } - // Pode haver múltiplas palavras de "present" (bit 31 = continua). - let mut present_words = Vec::new(); - let mut offset = 4; + // Pode haver múltiplas palavras de "present" (bit 31 = continua). Só a + // primeira descreve os campos padrão; as seguintes são namespaces de + // extensão que ficam depois dos campos que usamos. + let mut present = 0u32; + let mut offset = 4usize; + let mut first = true; loop { - if offset + 4 > buf.len() { + if offset + 4 > header_len { return None; } let word = u32::from_le_bytes([ @@ -39,36 +75,32 @@ pub fn parse(buf: &[u8]) -> Option { buf[offset + 2], buf[offset + 3], ]); - present_words.push(word); + if first { + present = word; + first = false; + } offset += 4; if word & (1 << 31) == 0 { break; } } - let present = present_words[0]; // só o primeiro conjunto de campos padrão nos interessa let mut info = RadiotapInfo { header_len, + present, ..Default::default() }; let mut cursor = offset; - // TSFT (u64, align 8) viria antes de Flags; para o nosso subconjunto de - // campos simplificamos assumindo captura via mac80211 (ordem padrão: - // TSFT, Flags, Rate, Channel, ..., AntSignal). Avançamos campo a campo - // respeitando alinhamento, sem travar se algum bit não estiver setado. macro_rules! align { - ($a:expr) => {{ - let rem = cursor % $a; - if rem != 0 { - cursor += $a - rem; - } - }}; + ($a:expr) => { + cursor = (cursor + ($a - 1)) & !($a - 1); + }; } - if present & (1 << 0) != 0 { + if present & BIT_TSFT != 0 { align!(8); - cursor += 8; // TSFT + cursor += 8; } if present & BIT_FLAGS != 0 { cursor += 1; @@ -78,19 +110,26 @@ pub fn parse(buf: &[u8]) -> Option { } if present & BIT_CHANNEL != 0 { align!(2); - if cursor + 4 <= buf.len() { + if cursor + 4 <= header_len { let freq = u16::from_le_bytes([buf[cursor], buf[cursor + 1]]); info.freq_mhz = Some(freq); + info.channel = freq_to_channel(freq); + info.band = Some(band_for_freq(freq)); } cursor += 4; } - // FHSS (bit 4) — raro, ignorado. - if present & (1 << 4) != 0 { + if present & BIT_FHSS != 0 { align!(2); cursor += 2; } - if present & BIT_ANTSIGNAL != 0 && cursor < buf.len() { - info.signal_dbm = Some(buf[cursor] as i8); + if present & BIT_ANTSIGNAL != 0 { + if cursor < header_len { + info.signal_dbm = Some(buf[cursor] as i8); + } + cursor += 1; + } + if present & BIT_ANTNOISE != 0 && cursor < header_len { + info.noise_dbm = Some(buf[cursor] as i8); } Some(info) @@ -98,9 +137,107 @@ pub fn parse(buf: &[u8]) -> Option { pub fn freq_to_channel(freq_mhz: u16) -> Option { match freq_mhz { - 2412..=2472 => Some(((freq_mhz - 2412) / 5 + 1) as u32), + 2412..=2472 => Some(((freq_mhz as u32 - 2412) / 5) + 1), 2484 => Some(14), - 5000..=5900 => Some(((freq_mhz - 5000) / 5) as u32), + // 4.9 GHz / segurança pública (canais 182–196). + 4910 => Some(182), + 4920 => Some(184), + 4940 => Some(188), + 4960 => Some(192), + 4980 => Some(196), + 5000..=5895 => Some((freq_mhz as u32 - 5000) / 5), + 5955..=7115 => Some((freq_mhz as u32 - 5950) / 5), _ => None, } } + +pub fn channel_to_freq(channel: u32, band: Band) -> Option { + match band { + Band::B2G4 => match channel { + 1..=13 => Some((2407 + channel * 5) as u16), + 14 => Some(2484), + _ => None, + }, + Band::B5 => match channel { + 182 => Some(4910), + 184 => Some(4920), + 188 => Some(4940), + 192 => Some(4960), + 196 => Some(4980), + 32..=177 => Some((5000 + channel * 5) as u16), + _ => None, + }, + Band::B6 => match channel { + 1..=233 => Some((5950 + channel * 5) as u16), + _ => None, + }, + Band::Unknown => None, + } +} + +pub fn band_for_freq(freq_mhz: u16) -> Band { + match freq_mhz { + 2400..=2500 => Band::B2G4, + 4900..=5925 => Band::B5, + 5926..=7125 => Band::B6, + _ => Band::Unknown, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn radiotap_with_signal(freq: u16, signal: i8) -> Vec { + // present: TSFT | Flags | Rate | Channel | AntSignal + let present: u32 = BIT_TSFT | BIT_FLAGS | BIT_RATE | BIT_CHANNEL | BIT_ANTSIGNAL; + let mut buf = Vec::new(); + buf.extend_from_slice(&[0, 0]); // version, pad + buf.extend_from_slice(&0u16.to_le_bytes()); // len (preenchido depois) + buf.extend_from_slice(&present.to_le_bytes()); + // present termina em 8; TSFT alinhado em 8 + buf.extend_from_slice(&0x1122_3344_5566_7788u64.to_le_bytes()); + buf.push(0x10); // flags + buf.push(0x02); // rate 1 Mb/s + // channel alinhado em 2 (cursor 18 já é par) + buf.extend_from_slice(&freq.to_le_bytes()); + buf.extend_from_slice(&0xa0u16.to_le_bytes()); + buf.push(signal as u8); + let len = buf.len() as u16; + buf[2..4].copy_from_slice(&len.to_le_bytes()); + buf + } + + #[test] + fn parses_signal_and_channel() { + let buf = radiotap_with_signal(2437, -42); + let info = parse(&buf).expect("radiotap válido"); + assert_eq!(info.freq_mhz, Some(2437)); + assert_eq!(info.channel, Some(6)); + assert_eq!(info.band, Some(Band::B2G4)); + assert_eq!(info.signal_dbm, Some(-42)); + assert_eq!(info.header_len, buf.len()); + } + + #[test] + fn rejects_invalid_version_and_length() { + assert!(parse(&[1, 0, 8, 0, 0, 0, 0, 0]).is_none()); + assert!(parse(&[0, 0, 4, 0, 0, 0, 0, 0]).is_none()); + assert!(parse(&[0, 0, 200, 0, 0, 0, 0, 0]).is_none()); + } + + #[test] + fn maps_frequencies_across_bands() { + assert_eq!(freq_to_channel(2412), Some(1)); + assert_eq!(freq_to_channel(2437), Some(6)); + assert_eq!(freq_to_channel(2484), Some(14)); + assert_eq!(freq_to_channel(5180), Some(36)); + assert_eq!(freq_to_channel(5955), Some(1)); + assert_eq!(freq_to_channel(6115), Some(33)); + assert_eq!(freq_to_channel(7115), Some(233)); + assert_eq!(freq_to_channel(1000), None); + assert_eq!(band_for_freq(2437), Band::B2G4); + assert_eq!(band_for_freq(5180), Band::B5); + assert_eq!(band_for_freq(6115), Band::B6); + } +} diff --git a/crates/sabia-core/src/scan.rs b/crates/sabia-core/src/scan.rs index b15c1f6..b84f826 100644 --- a/crates/sabia-core/src/scan.rs +++ b/crates/sabia-core/src/scan.rs @@ -1,9 +1,9 @@ -//! Agregação de quadros capturados em uma tabela de APs e clientes vistos, -//! no espírito do `airodump-ng` — mas como uma struct reaproveitável tanto -//! pela CLI quanto pela interface gráfica. +//! Agregação de quadros capturados em tabelas de redes, clientes e +//! handshakes — o coração do que a CLI e a interface mostram. use crate::dot11::{self, FrameKind, Security}; -use crate::radiotap; +use crate::handshake::{Handshake, HandshakeInfo, HandshakeTracker}; +use crate::radiotap::{self, Band}; use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; @@ -11,17 +11,73 @@ use std::time::{SystemTime, UNIX_EPOCH}; pub struct AccessPoint { pub bssid: String, pub ssid: Option, + pub hidden: bool, pub channel: Option, + pub band: Option, + pub freq_mhz: Option, pub security: Option, - pub last_signal_dbm: Option, - pub beacons_seen: u64, + pub cipher: Option, + pub akm: Vec, + pub pmf: bool, + pub wps: bool, + pub signal_dbm: Option, + pub max_signal_dbm: Option, + pub beacons: u64, + pub data_frames: u64, pub clients: Vec, + pub first_seen_unix: u64, pub last_seen_unix: u64, } +#[derive(Debug, Clone, serde::Serialize)] +pub struct Client { + pub mac: String, + pub bssid: Option, + pub signal_dbm: Option, + pub max_signal_dbm: Option, + pub packets: u64, + pub probes: Vec, + pub associated: bool, + pub first_seen_unix: u64, + pub last_seen_unix: u64, +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct IngestEvent { + pub eapol: bool, + pub handshake_updated: bool, + pub deauth: bool, +} + +#[derive(Debug, Clone, serde::Serialize)] +pub struct ScanSnapshot { + pub iface: String, + pub running: bool, + pub channel: Option, + pub stats: ScanStats, + pub access_points: Vec, + pub clients: Vec, + pub handshakes: Vec, +} + +#[derive(Debug, Clone, Copy, Default, serde::Serialize)] +pub struct ScanStats { + pub frames: u64, + pub eapol_frames: u64, + pub deauth_frames: u64, + pub aps: usize, + pub clients: usize, + pub handshakes: usize, +} + #[derive(Default)] pub struct ScanTable { aps: HashMap, + clients: HashMap, + handshakes: HandshakeTracker, + frames: u64, + eapol_frames: u64, + deauth_frames: u64, } impl ScanTable { @@ -29,91 +85,388 @@ impl ScanTable { Self::default() } - /// Processa um quadro radiotap+802.11 crú, atualizando a tabela. - /// Retorna `true` se o quadro continha um pacote EAPOL (indício de - /// handshake WPA em andamento — útil para o chamador decidir salvar em - /// pcap). + /// Processa um quadro radiotap+802.11 crú. Retorna `true` se o quadro + /// continha EAPOL (mantido para chamadores antigos). pub fn ingest(&mut self, raw: &[u8]) -> bool { - let rt = match radiotap::parse(raw) { - Some(rt) => rt, - None => return false, + let now = unix_now(); + self.ingest_at(raw, now).eapol + } + + pub fn ingest_at(&mut self, raw: &[u8], now: u64) -> IngestEvent { + let mut event = IngestEvent::default(); + let Some(rt) = radiotap::parse(raw) else { + return event; }; if rt.header_len >= raw.len() { - return false; + return event; } - let dot11_buf = &raw[rt.header_len..]; - let frame = match dot11::parse(dot11_buf) { - Some(f) => f, - None => return false, + let Some(frame) = dot11::parse(&raw[rt.header_len..]) else { + return event; }; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); + self.frames += 1; match frame.kind { FrameKind::Beacon | FrameKind::ProbeResponse => { - if let Some(bssid) = frame.bssid_str() { - let info = frame.management_info(); - let channel = info - .channel_ie - .or_else(|| rt.freq_mhz.and_then(radiotap::freq_to_channel)); - let entry = self - .aps - .entry(bssid.clone()) - .or_insert_with(|| AccessPoint { - bssid, - ssid: None, - channel: None, - security: None, - last_signal_dbm: None, - beacons_seen: 0, - clients: Vec::new(), - last_seen_unix: now, - }); - if let Some(ssid) = info.ssid { - entry.ssid = Some(ssid); + let Some(bssid) = frame.bssid_str() else { + return event; + }; + let info = frame.management_info(); + let channel = info + .channel + .or_else(|| rt.freq_mhz.and_then(radiotap::freq_to_channel)); + let band = rt.freq_mhz.map(radiotap::band_for_freq); + let ap = self + .aps + .entry(bssid.clone()) + .or_insert_with(|| AccessPoint { + bssid: bssid.clone(), + ssid: None, + hidden: false, + channel: None, + band: None, + freq_mhz: None, + security: None, + cipher: None, + akm: Vec::new(), + pmf: false, + wps: false, + signal_dbm: None, + max_signal_dbm: None, + beacons: 0, + data_frames: 0, + clients: Vec::new(), + first_seen_unix: now, + last_seen_unix: now, + }); + if let Some(ssid) = info.ssid.clone() { + if !ssid.is_empty() || ap.ssid.is_none() { + ap.ssid = Some(ssid); } - if channel.is_some() { - entry.channel = channel; - } - entry.security = info.security.map(security_label); - entry.last_signal_dbm = rt.signal_dbm; - entry.beacons_seen += 1; - entry.last_seen_unix = now; + ap.hidden = info.hidden; + } + if channel.is_some() { + ap.channel = channel; + } + if rt.freq_mhz.is_some() { + ap.freq_mhz = rt.freq_mhz; + ap.band = band; + } + ap.security = info.security.map(security_label); + ap.cipher = info.cipher.clone(); + ap.akm = info.akm.clone(); + ap.pmf = info.pmf; + ap.wps = info.wps; + ap.signal_dbm = rt.signal_dbm; + if let Some(sig) = rt.signal_dbm { + ap.max_signal_dbm = Some(ap.max_signal_dbm.map_or(sig, |m| m.max(sig))); + } + ap.beacons += 1; + ap.last_seen_unix = now; + + if let Some(ssid) = ap.ssid.clone().filter(|s| !s.is_empty()) { + self.handshakes.update_ssid(&bssid, &ssid); } } - FrameKind::Data => { - // Associa o cliente (endereço de origem) ao BSSID observado, - // quando a direção do quadro indicar que vem de uma estação. - if let (Some(bssid), Some(src)) = (frame.bssid_str(), frame.src_str()) { + FrameKind::ProbeRequest => { + let Some(client) = frame.src_str() else { + return event; + }; + let info = frame.management_info(); + let c = self.touch_client(&client, now, rt.signal_dbm); + c.packets += 1; + if let Some(ssid) = info.ssid.filter(|s| !s.is_empty()) { + if !c.probes.contains(&ssid) { + c.probes.push(ssid); + } + } + } + FrameKind::AssociationRequest => { + let client = frame.src_str(); + let bssid = frame + .addr1 + .map(|m| dot11::mac_to_string(&m)) + .or_else(|| frame.bssid_str()); + if let (Some(client), Some(bssid)) = (client, bssid) { + let c = self.touch_client(&client, now, rt.signal_dbm); + c.packets += 1; + c.bssid = Some(bssid.clone()); + c.associated = true; if let Some(ap) = self.aps.get_mut(&bssid) { - if src != bssid && !ap.clients.contains(&src) { - ap.clients.push(src); + if !ap.clients.contains(&client) { + ap.clients.push(client); } } } - return dot11::is_eapol(frame.body); + } + FrameKind::Data => { + let bssid = match (frame.to_ds, frame.from_ds) { + (true, false) => frame.addr1.map(|m| dot11::mac_to_string(&m)), + (false, true) => frame.addr2.map(|m| dot11::mac_to_string(&m)), + _ => frame.bssid_str(), + }; + let client = match (frame.to_ds, frame.from_ds) { + (true, false) => frame.src_str(), + (false, true) => frame.dst_str(), + _ => frame.src_str(), + }; + if let (Some(client), Some(bssid)) = (client.clone(), bssid.clone()) { + if client != bssid { + let c = self.touch_client(&client, now, rt.signal_dbm); + c.packets += 1; + c.bssid = Some(bssid.clone()); + c.associated = true; + if let Some(ap) = self.aps.get_mut(&bssid) { + ap.data_frames += 1; + if !ap.clients.contains(&client) { + ap.clients.push(client.clone()); + } + } + } + } + if frame.eapol().is_some() { + event.eapol = true; + self.eapol_frames += 1; + let hint = bssid + .as_deref() + .and_then(|b| self.aps.get(b)) + .and_then(|ap| ap.ssid.clone()) + .filter(|s| !s.is_empty()); + if self.handshakes.ingest(&frame, raw, hint.as_deref(), now) { + event.handshake_updated = true; + } + } + } + FrameKind::Deauth | FrameKind::Disassoc => { + self.deauth_frames += 1; + event.deauth = true; + let a1 = frame.addr1.map(|m| dot11::mac_to_string(&m)); + let a2 = frame.addr2.map(|m| dot11::mac_to_string(&m)); + let bssid = frame.bssid_str(); + let client = match (&a1, &a2, &bssid) { + (Some(a1), Some(_), Some(b)) if a1 != b => Some(a1.clone()), + (Some(_), Some(a2), Some(b)) if a2 != b => Some(a2.clone()), + _ => None, + }; + if let Some(client) = client { + if let Some(c) = self.clients.get_mut(&client) { + c.associated = false; + c.last_seen_unix = now; + } + } } _ => {} } - false + + event + } + + fn touch_client(&mut self, mac: &str, now: u64, signal: Option) -> &mut Client { + let c = self + .clients + .entry(mac.to_string()) + .or_insert_with(|| Client { + mac: mac.to_string(), + bssid: None, + signal_dbm: None, + max_signal_dbm: None, + packets: 0, + probes: Vec::new(), + associated: false, + first_seen_unix: now, + last_seen_unix: now, + }); + c.last_seen_unix = now; + c.signal_dbm = signal; + if let Some(sig) = signal { + c.max_signal_dbm = Some(c.max_signal_dbm.map_or(sig, |m| m.max(sig))); + } + c } pub fn access_points(&self) -> Vec { let mut v: Vec<_> = self.aps.values().cloned().collect(); - v.sort_by_key(|a| std::cmp::Reverse(a.beacons_seen)); + v.sort_by(|a, b| { + b.beacons + .cmp(&a.beacons) + .then_with(|| a.ssid.cmp(&b.ssid)) + .then_with(|| a.bssid.cmp(&b.bssid)) + }); v } + + pub fn clients(&self) -> Vec { + let mut v: Vec<_> = self.clients.values().cloned().collect(); + v.sort_by(|a, b| b.packets.cmp(&a.packets).then_with(|| a.mac.cmp(&b.mac))); + v + } + + pub fn handshakes(&self) -> Vec { + self.handshakes.infos() + } + + pub fn handshake(&self, bssid: &str, station: &str) -> Option<&Handshake> { + self.handshakes.get(bssid, station) + } + + /// Handshakes completos (com quadros crus) para exportação. + pub fn handshake_entries(&self) -> Vec { + self.handshakes.all().into_iter().cloned().collect() + } + + pub fn first_crackable( + &self, + bssid: Option<&str>, + station: Option<&str>, + ) -> Option<&Handshake> { + self.handshakes.first_crackable(bssid, station) + } + + pub fn set_password(&mut self, bssid: &str, station: &str, password: &str) { + self.handshakes.set_password(bssid, station, password); + } + + pub fn stats(&self) -> ScanStats { + ScanStats { + frames: self.frames, + eapol_frames: self.eapol_frames, + deauth_frames: self.deauth_frames, + aps: self.aps.len(), + clients: self.clients.len(), + handshakes: self.handshakes.len(), + } + } } fn security_label(s: Security) -> String { match s { - Security::Open => "Aberta".into(), + Security::Open => "Open".into(), Security::Wep => "WEP".into(), Security::Wpa => "WPA".into(), Security::Wpa2 => "WPA2".into(), + Security::Wpa2Enterprise => "WPA2-EAP".into(), Security::Wpa3 => "WPA3".into(), + Security::Wpa3Enterprise => "WPA3-EAP".into(), + Security::Owe => "OWE".into(), + Security::Wpa2Wpa3 => "WPA2/WPA3".into(), + } +} + +fn unix_now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testutil::{ + ap, beacon_frame, frame_with_eapol, handshake_frames, probe_request, radiotap_wrap, sta, + }; + + fn beacon_raw(ssid: &str, akms: &[u8]) -> Vec { + let frame = beacon_frame(ap(), ssid, 6, akms); + radiotap_wrap(&frame, 2437, -40) + } + + #[test] + fn builds_ap_table_from_beacon() { + let mut table = ScanTable::new(); + table.ingest_at(&beacon_raw("Casa", &[2]), 100); + let aps = table.access_points(); + assert_eq!(aps.len(), 1); + let ap = &aps[0]; + assert_eq!(ap.ssid.as_deref(), Some("Casa")); + assert_eq!(ap.bssid, "00:11:22:33:44:55"); + assert_eq!(ap.channel, Some(6)); + assert_eq!(ap.band, Some(Band::B2G4)); + assert_eq!(ap.security.as_deref(), Some("WPA2")); + assert_eq!(ap.cipher.as_deref(), Some("CCMP")); + assert_eq!(ap.signal_dbm, Some(-40)); + assert_eq!(ap.beacons, 1); + assert_eq!(ap.first_seen_unix, 100); + } + + #[test] + fn tracks_probes_and_data_clients() { + let mut table = ScanTable::new(); + table.ingest_at(&beacon_raw("Casa", &[2]), 1); + let probe = radiotap_wrap(&probe_request(sta(), "Casa"), 2437, -55); + table.ingest_at(&probe, 2); + + let payload = [0xAA, 0xAA, 0x03, 0, 0, 0, 0x08, 0x00, 1, 2, 3]; + let data = crate::testutil::data_frame(false, true, sta(), ap(), [0x33; 6], &payload); + let data = radiotap_wrap(&data, 2437, -50); + table.ingest_at(&data, 3); + + let clients = table.clients(); + assert_eq!(clients.len(), 1); + let c = &clients[0]; + assert_eq!(c.mac, "66:77:88:99:aa:bb"); + assert_eq!(c.bssid.as_deref(), Some("00:11:22:33:44:55")); + assert!(c.associated); + assert!(c.probes.contains(&"Casa".to_string())); + assert_eq!(c.packets, 2); + assert_eq!(table.access_points()[0].clients.len(), 1); + assert_eq!(table.stats().frames, 3); + } + + #[test] + fn captures_handshake_from_raw_frames() { + let mut table = ScanTable::new(); + table.ingest_at(&beacon_raw("RedeWPA", &[2]), 1); + let (m1, m2) = handshake_frames("RedeWPA", "password", 2, false); + + let f1 = frame_with_eapol(true, ap(), sta(), [0x33; 6], &m1); + let raw1 = radiotap_wrap(&f1, 2437, -40); + let e1 = table.ingest_at(&raw1, 2); + assert!(e1.eapol); + assert!(e1.handshake_updated); + + let f2 = frame_with_eapol(false, ap(), sta(), [0x33; 6], &m2); + let raw2 = radiotap_wrap(&f2, 2437, -41); + let e2 = table.ingest_at(&raw2, 3); + assert!(e2.eapol); + + let hs = table.handshakes(); + assert_eq!(hs.len(), 1); + assert!(hs[0].complete); + assert!(hs[0].crackable); + assert_eq!(hs[0].ssid.as_deref(), Some("RedeWPA")); + assert_eq!(hs[0].messages, "1,2"); + assert!(table.first_crackable(None, None).is_some()); + assert!(table + .first_crackable(Some("00:11:22:33:44:55"), None) + .is_some()); + assert!(table + .first_crackable(Some("ff:ff:ff:ff:ff:ff"), None) + .is_none()); + + table.set_password("00:11:22:33:44:55", "66:77:88:99:aa:bb", "password"); + assert_eq!(table.handshakes()[0].password.as_deref(), Some("password")); + assert_eq!(table.stats().eapol_frames, 2); + } + + #[test] + fn counts_deauth_and_marks_client_disconnected() { + let mut table = ScanTable::new(); + table.ingest_at(&beacon_raw("Casa", &[2]), 1); + let payload = [0xAA, 0xAA, 0x03, 0, 0, 0, 0x08, 0x00, 1]; + let data = crate::testutil::data_frame(false, true, sta(), ap(), [0x33; 6], &payload); + table.ingest_at(&radiotap_wrap(&data, 2437, -50), 2); + + let mut deauth = Vec::new(); + deauth.extend_from_slice(&0x00c0u16.to_le_bytes()); + deauth.extend_from_slice(&0u16.to_le_bytes()); + deauth.extend_from_slice(&sta()); + deauth.extend_from_slice(&ap()); + deauth.extend_from_slice(&ap()); + deauth.extend_from_slice(&[0u8; 2]); + deauth.extend_from_slice(&7u16.to_le_bytes()); + let event = table.ingest_at(&radiotap_wrap(&deauth, 2437, -50), 3); + assert!(event.deauth); + assert_eq!(table.stats().deauth_frames, 1); + assert!(!table.clients()[0].associated); } } diff --git a/crates/sabia-core/src/testutil.rs b/crates/sabia-core/src/testutil.rs new file mode 100644 index 0000000..dceec85 --- /dev/null +++ b/crates/sabia-core/src/testutil.rs @@ -0,0 +1,221 @@ +//! Helpers de teste: quadros 802.11/EAPOL sintéticos, radiotap e handshakes +//! válidos gerados a partir de uma senha conhecida. Só existe em `cfg(test)`. + +use crate::crack::{compute_mic, compute_pmkid, compute_ptk, derive_pmk}; +use crate::dot11; +use crate::eapol::{self, KEY_DATA_OFFSET, MIC_OFFSET}; +use crate::handshake::Handshake; + +const AP: [u8; 6] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]; +const STA: [u8; 6] = [0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb]; + +pub fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> Vec { + use super::eapol::EAPOL_TYPE_KEY; + let mut key_info = keyver as u16 | 0x0008; // pairwise + match message { + 1 => key_info |= 0x0080, + 2 => key_info |= 0x0100, + 3 => key_info |= 0x0080 | 0x0100 | 0x0040, + 4 => key_info |= 0x0100 | 0x0200, + _ => {} + } + let total = KEY_DATA_OFFSET + key_data.len(); + let mut f = vec![0u8; total]; + f[0] = 2; + f[1] = EAPOL_TYPE_KEY; + f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes()); + f[4] = 2; + f[5..7].copy_from_slice(&key_info.to_le_bytes()); + f[7..9].copy_from_slice(&16u16.to_le_bytes()); + f[9..17].copy_from_slice(&1u64.to_le_bytes()); + f[17..49].copy_from_slice(&nonce); + f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes()); + f[KEY_DATA_OFFSET..].copy_from_slice(key_data); + f +} + +pub fn frame_with_eapol( + from_ap: bool, + ap: [u8; 6], + sta: [u8; 6], + da: [u8; 6], + eapol: &[u8], +) -> Vec { + let mut f = Vec::new(); + let fc: u16 = if from_ap { 0x0208 } else { 0x0108 }; + f.extend_from_slice(&fc.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + if from_ap { + f.extend_from_slice(&sta); // addr1 = destino + f.extend_from_slice(&ap); // addr2 = BSSID + } else { + f.extend_from_slice(&ap); // addr1 = BSSID + f.extend_from_slice(&sta); // addr2 = origem + } + f.extend_from_slice(&da); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x88, 0x8e]); + f.extend_from_slice(eapol); + f +} + +pub fn radiotap_wrap(dot11: &[u8], freq: u16, signal: i8) -> Vec { + // present: Channel | AntSignal + let present: u32 = (1 << 3) | (1 << 5); + let mut f = vec![0u8, 0, 13, 0]; + f.extend_from_slice(&present.to_le_bytes()); + f.extend_from_slice(&freq.to_le_bytes()); + f.extend_from_slice(&0xa0u16.to_le_bytes()); + f.push(signal as u8); + f.extend_from_slice(dot11); + f +} + +fn ie(tag: u8, val: &[u8]) -> Vec { + let mut v = vec![tag, val.len() as u8]; + v.extend_from_slice(val); + v +} + +fn rsn_ie(akms: &[u8]) -> Vec { + let mut val = Vec::new(); + val.extend_from_slice(&1u16.to_le_bytes()); + val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); + val.extend_from_slice(&1u16.to_le_bytes()); + val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); + val.extend_from_slice(&(akms.len() as u16).to_le_bytes()); + for akm in akms { + val.extend_from_slice(&[0x00, 0x0f, 0xac, *akm]); + } + val.extend_from_slice(&0x0040u16.to_le_bytes()); + ie(48, &val) +} + +pub fn beacon_frame(bssid: [u8; 6], ssid: &str, channel: u8, akms: &[u8]) -> Vec { + let mut f = Vec::new(); + f.extend_from_slice(&0x0080u16.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&[0xff; 6]); + f.extend_from_slice(&bssid); + f.extend_from_slice(&bssid); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&[0u8; 8]); + f.extend_from_slice(&100u16.to_le_bytes()); + f.extend_from_slice(&0x0031u16.to_le_bytes()); + f.extend_from_slice(&ie(0, ssid.as_bytes())); + f.extend_from_slice(&ie(3, &[channel])); + if !akms.is_empty() { + f.extend_from_slice(&rsn_ie(akms)); + } + f +} + +pub fn probe_request(station: [u8; 6], ssid: &str) -> Vec { + let mut f = Vec::new(); + f.extend_from_slice(&0x0040u16.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&[0xff; 6]); + f.extend_from_slice(&station); + f.extend_from_slice(&[0xff; 6]); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&ie(0, ssid.as_bytes())); + f +} + +pub fn data_frame( + to_ds: bool, + from_ds: bool, + addr1: [u8; 6], + addr2: [u8; 6], + addr3: [u8; 6], + payload: &[u8], +) -> Vec { + let mut fc: u16 = 0x0008; + if to_ds { + fc |= 0x0100; + } + if from_ds { + fc |= 0x0200; + } + let mut f = Vec::new(); + f.extend_from_slice(&fc.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&addr1); + f.extend_from_slice(&addr2); + f.extend_from_slice(&addr3); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(payload); + f +} + +/// Gera (msg1, msg2) válidas para a senha informada, com MIC correto. +pub fn handshake_frames( + ssid: &str, + password: &str, + keyver: u8, + with_pmkid: bool, +) -> (Vec, Vec) { + let anonce = [0xaa; 32]; + let snonce = [0xbb; 32]; + let pmk = derive_pmk(password, ssid); + let mut m1_key_data = Vec::new(); + if with_pmkid { + let pmkid = compute_pmkid(&pmk, &AP, &STA); + m1_key_data.extend_from_slice(&[0xdd, 20, 0x00, 0x0f, 0xac, 0x04]); + m1_key_data.extend_from_slice(&pmkid); + } + let m1 = key_frame(1, keyver, anonce, &m1_key_data); + let mut m2 = key_frame(2, keyver, snonce, &[]); + let ptk = compute_ptk(&pmk, &AP, &STA, &anonce, &snonce); + let mic = compute_mic(keyver, &ptk[..16], &m2, MIC_OFFSET).expect("mic"); + m2[MIC_OFFSET..MIC_OFFSET + 16].copy_from_slice(&mic); + (m1, m2) +} + +/// Monta um `Handshake` diretamente dos quadros EAPOL, para testes de quebra +/// sem depender do pipeline de captura. +pub fn synthetic_handshake( + ssid: &str, + bssid: &str, + station: &str, + m1: &[u8], + m2: &[u8], +) -> Handshake { + let k1 = eapol::parse(m1).expect("msg1"); + let k2 = if m2.is_empty() { + None + } else { + Some(eapol::parse(m2).expect("msg2")) + }; + let mut messages = 1u8; + if k2.is_some() { + messages |= 2; + } + let hs = Handshake { + bssid: bssid.to_string(), + station: station.to_string(), + ssid: Some(ssid.to_string()), + anonce: Some(k1.nonce), + snonce: k2.as_ref().map(|k| k.nonce), + pmkid: k1.pmkid, + key_version: k1.key_version, + messages, + eapol_msg2: k2.as_ref().map(|k| k.frame.clone()), + raw: Vec::new(), + first_seen_unix: 1, + last_seen_unix: 2, + password: None, + }; + // Exercita o parser de MAC do alvo. + assert!(dot11::parse_mac(&hs.bssid).is_some()); + assert!(dot11::parse_mac(&hs.station).is_some()); + hs +} + +pub fn ap() -> [u8; 6] { + AP +} + +pub fn sta() -> [u8; 6] { + STA +} diff --git a/crates/sabia-core/tests/audit_trail.rs b/crates/sabia-core/tests/audit_trail.rs new file mode 100644 index 0000000..64b2a17 --- /dev/null +++ b/crates/sabia-core/tests/audit_trail.rs @@ -0,0 +1,77 @@ +//! Integração: trilha de auditoria encadeada por hash — grava entradas, +//! confirma que a cadeia valida, depois adultera um registro no disco e +//! confirma que a verificação detecta a falha. + +mod common; + +use sabia_core::audit::AuditLog; +use std::io::Write; + +#[test] +fn chain_validates_after_recording_entries() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("audit.log"); + + { + let mut log = AuditLog::open(&path).expect("open"); + log.record("monitor-on", "wlan0", "modo monitor ativado") + .expect("record 1"); + log.record("scan-start", "wlan0", "início da captura") + .expect("record 2"); + log.record("deauth", "00:11:22:33:44:55", "teste de deautenticação") + .expect("record 3"); + } + + let entries = AuditLog::read_entries(&path, None).expect("read_entries"); + assert_eq!(entries.len(), 3); + assert_eq!(entries[0].action, "monitor-on"); + assert_eq!(entries[2].action, "deauth"); + // cadeia encadeada corretamente + assert_eq!(entries[1].prev_hash, entries[0].hash); + assert_eq!(entries[2].prev_hash, entries[1].hash); + + let result = AuditLog::verify(&path).expect("verify"); + assert_eq!( + result, None, + "cadeia íntegra não deve apontar entrada inválida" + ); +} + +#[test] +fn tampering_with_an_entry_breaks_verification() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("audit.log"); + + { + let mut log = AuditLog::open(&path).expect("open"); + log.record("monitor-on", "wlan0", "modo monitor ativado") + .expect("record 1"); + log.record("scan-start", "wlan0", "início da captura") + .expect("record 2"); + log.record("scan-stop", "wlan0", "fim da captura") + .expect("record 3"); + } + + // Confirma que, intacta, a cadeia passa. + assert_eq!(AuditLog::verify(&path).expect("verify ok"), None); + + // Adultera o "detail" da segunda entrada sem recalcular o hash, + // simulando uma edição retroativa do log. + let contents = std::fs::read_to_string(&path).expect("read"); + let mut lines: Vec = contents.lines().map(|l| l.to_string()).collect(); + assert_eq!(lines.len(), 3); + assert!(lines[1].contains("início da captura")); + lines[1] = lines[1].replace("início da captura", "evento adulterado"); + + let mut file = std::fs::File::create(&path).expect("recreate"); + for line in &lines { + writeln!(file, "{line}").expect("write tampered line"); + } + + let result = AuditLog::verify(&path).expect("verify"); + assert_eq!( + result, + Some(1), + "verificação deveria apontar a entrada adulterada (índice 1)" + ); +} diff --git a/crates/sabia-core/tests/common/mod.rs b/crates/sabia-core/tests/common/mod.rs new file mode 100644 index 0000000..55772fc --- /dev/null +++ b/crates/sabia-core/tests/common/mod.rs @@ -0,0 +1,239 @@ +#![allow(dead_code)] +//! Helpers de teste de integração: quadros 802.11/EAPOL sintéticos, +//! radiotap e handshakes válidos gerados a partir de uma senha conhecida. +//! +//! Porta a lógica de `crates/sabia-core/src/testutil.rs` (que só existe em +//! `#[cfg(test)]` e por isso não fica visível aqui), usando só a API +//! pública de `sabia_core`. + +use sabia_core::crack::{compute_mic, compute_pmkid, compute_ptk, derive_pmk}; +use sabia_core::dot11; +use sabia_core::eapol::{self, EAPOL_TYPE_KEY, KEY_DATA_OFFSET, MIC_OFFSET}; +use sabia_core::handshake::Handshake; + +pub const AP: [u8; 6] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]; +pub const STA: [u8; 6] = [0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb]; + +pub fn ap() -> [u8; 6] { + AP +} + +pub fn sta() -> [u8; 6] { + STA +} + +pub fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> Vec { + let mut key_info = keyver as u16 | 0x0008; // pairwise + match message { + 1 => key_info |= 0x0080, + 2 => key_info |= 0x0100, + 3 => key_info |= 0x0080 | 0x0100 | 0x0040, + 4 => key_info |= 0x0100 | 0x0200, + _ => {} + } + let total = KEY_DATA_OFFSET + key_data.len(); + let mut f = vec![0u8; total]; + f[0] = 2; + f[1] = EAPOL_TYPE_KEY; + f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes()); + f[4] = 2; + f[5..7].copy_from_slice(&key_info.to_le_bytes()); + f[7..9].copy_from_slice(&16u16.to_le_bytes()); + f[9..17].copy_from_slice(&1u64.to_le_bytes()); + f[17..49].copy_from_slice(&nonce); + f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes()); + f[KEY_DATA_OFFSET..].copy_from_slice(key_data); + f +} + +pub fn frame_with_eapol( + from_ap: bool, + ap: [u8; 6], + sta: [u8; 6], + da: [u8; 6], + eapol: &[u8], +) -> Vec { + let mut f = Vec::new(); + let fc: u16 = if from_ap { 0x0208 } else { 0x0108 }; + f.extend_from_slice(&fc.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + if from_ap { + f.extend_from_slice(&sta); // addr1 = destino + f.extend_from_slice(&ap); // addr2 = BSSID + } else { + f.extend_from_slice(&ap); // addr1 = BSSID + f.extend_from_slice(&sta); // addr2 = origem + } + f.extend_from_slice(&da); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x88, 0x8e]); + f.extend_from_slice(eapol); + f +} + +pub fn radiotap_wrap(dot11: &[u8], freq: u16, signal: i8) -> Vec { + // present: Channel | AntSignal + let present: u32 = (1 << 3) | (1 << 5); + let mut f = vec![0u8, 0, 13, 0]; + f.extend_from_slice(&present.to_le_bytes()); + f.extend_from_slice(&freq.to_le_bytes()); + f.extend_from_slice(&0xa0u16.to_le_bytes()); + f.push(signal as u8); + f.extend_from_slice(dot11); + f +} + +fn ie(tag: u8, val: &[u8]) -> Vec { + let mut v = vec![tag, val.len() as u8]; + v.extend_from_slice(val); + v +} + +fn rsn_ie(akms: &[u8]) -> Vec { + let mut val = Vec::new(); + val.extend_from_slice(&1u16.to_le_bytes()); + val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); + val.extend_from_slice(&1u16.to_le_bytes()); + val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]); + val.extend_from_slice(&(akms.len() as u16).to_le_bytes()); + for akm in akms { + val.extend_from_slice(&[0x00, 0x0f, 0xac, *akm]); + } + val.extend_from_slice(&0x0040u16.to_le_bytes()); + ie(48, &val) +} + +pub fn beacon_frame(bssid: [u8; 6], ssid: &str, channel: u8, akms: &[u8]) -> Vec { + let mut f = Vec::new(); + f.extend_from_slice(&0x0080u16.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&[0xff; 6]); + f.extend_from_slice(&bssid); + f.extend_from_slice(&bssid); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&[0u8; 8]); + f.extend_from_slice(&100u16.to_le_bytes()); + f.extend_from_slice(&0x0031u16.to_le_bytes()); + f.extend_from_slice(&ie(0, ssid.as_bytes())); + f.extend_from_slice(&ie(3, &[channel])); + if !akms.is_empty() { + f.extend_from_slice(&rsn_ie(akms)); + } + f +} + +pub fn probe_request(station: [u8; 6], ssid: &str) -> Vec { + let mut f = Vec::new(); + f.extend_from_slice(&0x0040u16.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&[0xff; 6]); + f.extend_from_slice(&station); + f.extend_from_slice(&[0xff; 6]); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(&ie(0, ssid.as_bytes())); + f +} + +pub fn data_frame( + to_ds: bool, + from_ds: bool, + addr1: [u8; 6], + addr2: [u8; 6], + addr3: [u8; 6], + payload: &[u8], +) -> Vec { + let mut fc: u16 = 0x0008; + if to_ds { + fc |= 0x0100; + } + if from_ds { + fc |= 0x0200; + } + let mut f = Vec::new(); + f.extend_from_slice(&fc.to_le_bytes()); + f.extend_from_slice(&0u16.to_le_bytes()); + f.extend_from_slice(&addr1); + f.extend_from_slice(&addr2); + f.extend_from_slice(&addr3); + f.extend_from_slice(&[0u8; 2]); + f.extend_from_slice(payload); + f +} + +/// Gera (msg1, msg2) válidas para a senha informada, com MIC correto. +pub fn handshake_frames( + ssid: &str, + password: &str, + keyver: u8, + with_pmkid: bool, +) -> (Vec, Vec) { + let anonce = [0xaa; 32]; + let snonce = [0xbb; 32]; + let pmk = derive_pmk(password, ssid); + let mut m1_key_data = Vec::new(); + if with_pmkid { + let pmkid = compute_pmkid(&pmk, &AP, &STA); + m1_key_data.extend_from_slice(&[0xdd, 20, 0x00, 0x0f, 0xac, 0x04]); + m1_key_data.extend_from_slice(&pmkid); + } + let m1 = key_frame(1, keyver, anonce, &m1_key_data); + let mut m2 = key_frame(2, keyver, snonce, &[]); + let ptk = compute_ptk(&pmk, &AP, &STA, &anonce, &snonce); + let mic = compute_mic(keyver, &ptk[..16], &m2, MIC_OFFSET).expect("mic"); + m2[MIC_OFFSET..MIC_OFFSET + 16].copy_from_slice(&mic); + (m1, m2) +} + +/// Monta quadros radiotap+802.11 completos (prontos para `ScanTable::ingest`) +/// com o 4-way handshake entre `AP` e `STA` para o SSID/senha dados. +pub fn handshake_radiotap_frames( + ssid: &str, + password: &str, + keyver: u8, + with_pmkid: bool, +) -> (Vec, Vec) { + let (m1, m2) = handshake_frames(ssid, password, keyver, with_pmkid); + let f1 = frame_with_eapol(true, AP, STA, STA, &m1); + let f2 = frame_with_eapol(false, AP, STA, AP, &m2); + (radiotap_wrap(&f1, 2437, -40), radiotap_wrap(&f2, 2437, -42)) +} + +/// Monta um `Handshake` diretamente dos quadros EAPOL, para testes de quebra +/// sem depender do pipeline de captura. +pub fn synthetic_handshake( + ssid: &str, + bssid: &str, + station: &str, + m1: &[u8], + m2: &[u8], +) -> Handshake { + let k1 = eapol::parse(m1).expect("msg1"); + let k2 = if m2.is_empty() { + None + } else { + Some(eapol::parse(m2).expect("msg2")) + }; + let mut messages = 1u8; + if k2.is_some() { + messages |= 2; + } + let hs = Handshake { + bssid: bssid.to_string(), + station: station.to_string(), + ssid: Some(ssid.to_string()), + anonce: Some(k1.nonce), + snonce: k2.as_ref().map(|k| k.nonce), + pmkid: k1.pmkid, + key_version: k1.key_version, + messages, + eapol_msg2: k2.as_ref().map(|k| k.frame.clone()), + raw: Vec::new(), + first_seen_unix: 1, + last_seen_unix: 2, + password: None, + }; + // Exercita o parser de MAC do alvo. + assert!(dot11::parse_mac(&hs.bssid).is_some()); + assert!(dot11::parse_mac(&hs.station).is_some()); + hs +} diff --git a/crates/sabia-core/tests/crack_roundtrip.rs b/crates/sabia-core/tests/crack_roundtrip.rs new file mode 100644 index 0000000..3519625 --- /dev/null +++ b/crates/sabia-core/tests/crack_roundtrip.rs @@ -0,0 +1,80 @@ +//! Integração: handshake sintético (gerado com senha conhecida) -> montagem +//! de `CrackTarget` -> `verify()`, para os dois métodos (EAPOL 4-way e +//! PMKID), cobrindo senha certa e errada. + +mod common; + +use sabia_core::crack::{compute_mic, compute_pmkid, compute_ptk, derive_pmk, CrackTarget}; + +const BSSID: &str = "00:11:22:33:44:55"; +const STATION: &str = "66:77:88:99:aa:bb"; + +#[test] +fn eapol_4way_roundtrip_verifies_password() { + let (m1, m2) = common::handshake_frames("MinhaRede", "senha-correta", 2, false); + let hs = common::synthetic_handshake("MinhaRede", BSSID, STATION, &m1, &m2); + + let target = CrackTarget::from_handshake(&hs).expect("alvo EAPOL válido"); + assert_eq!(target.method(), "EAPOL/CCMP"); + assert!(target.verify("senha-correta")); + assert!(!target.verify("senha-errada")); +} + +#[test] +fn pmkid_roundtrip_verifies_password() { + let (m1, _m2) = common::handshake_frames("MinhaRede", "senha-correta", 2, true); + let hs = common::synthetic_handshake("MinhaRede", BSSID, STATION, &m1, &[]); + + let target = CrackTarget::from_handshake(&hs).expect("alvo PMKID válido"); + assert_eq!(target.method(), "PMKID"); + assert!(target.verify("senha-correta")); + assert!(!target.verify("senha-errada")); +} + +#[test] +fn tkip_keyversion_roundtrip_verifies_password() { + let (m1, m2) = common::handshake_frames("RedeAntiga", "outra-senha", 1, false); + let hs = common::synthetic_handshake("RedeAntiga", BSSID, STATION, &m1, &m2); + + let target = CrackTarget::from_handshake(&hs).expect("alvo EAPOL/TKIP válido"); + assert_eq!(target.method(), "EAPOL/TKIP"); + assert!(target.verify("outra-senha")); + assert!(!target.verify("senha-errada")); +} + +#[test] +fn incomplete_handshake_without_pmkid_or_msg2_has_no_target() { + let (m1, _m2) = common::handshake_frames("MinhaRede", "senha-correta", 2, false); + // Sem msg2 e sem PMKID: não há prova suficiente para quebrar. + let hs = common::synthetic_handshake("MinhaRede", BSSID, STATION, &m1, &[]); + assert!(CrackTarget::from_handshake(&hs).is_none()); +} + +#[test] +fn derive_pmk_compute_ptk_and_mic_are_consistent() { + let ssid = "VetorDireto"; + let password = "senha-direta"; + let anonce = [0xaa; 32]; + let snonce = [0xbb; 32]; + + let pmk = derive_pmk(password, ssid); + let ptk = compute_ptk(&pmk, &common::ap(), &common::sta(), &anonce, &snonce); + + // MIC calculado sobre um quadro msg2 qualquer, zerando o campo MIC antes. + let mut m2 = common::key_frame(2, 2, snonce, &[]); + let mic = compute_mic(2, &ptk[..16], &m2, sabia_core::eapol::MIC_OFFSET).expect("mic"); + m2[sabia_core::eapol::MIC_OFFSET..sabia_core::eapol::MIC_OFFSET + 16].copy_from_slice(&mic); + + // Recalculando o MIC sobre o quadro já preenchido deve bater de novo + // (compute_mic zera o campo internamente antes de recalcular). + let mic_again = compute_mic(2, &ptk[..16], &m2, sabia_core::eapol::MIC_OFFSET).expect("mic"); + assert_eq!(mic, mic_again); + + // PMKID direto também deve ser determinístico e usável no verify(). + let pmkid = compute_pmkid(&pmk, &common::ap(), &common::sta()); + assert_eq!( + pmkid, + compute_pmkid(&pmk, &common::ap(), &common::sta()), + "PMKID deve ser determinístico" + ); +} diff --git a/crates/sabia-core/tests/pcap_roundtrip.rs b/crates/sabia-core/tests/pcap_roundtrip.rs new file mode 100644 index 0000000..27965d1 --- /dev/null +++ b/crates/sabia-core/tests/pcap_roundtrip.rs @@ -0,0 +1,83 @@ +//! Integração: grava quadros com `PcapWriter`, lê de volta com +//! `PcapReader` e confere que batem byte a byte; depois confirma que +//! `Engine::analyze_pcap` encontra redes e handshakes no arquivo recém-escrito. + +mod common; + +use sabia_core::engine::Engine; +use sabia_core::pcapreader::PcapReader; +use sabia_core::pcapwriter::PcapWriter; + +#[test] +fn writes_and_reads_back_identical_frames() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join("captura.pcap"); + + let beacon = common::radiotap_wrap( + &common::beacon_frame(common::ap(), "PcapRede", 1, &[2]), + 2412, + -30, + ); + let probe = common::radiotap_wrap(&common::probe_request(common::sta(), "PcapRede"), 2412, -45); + + { + let mut writer = PcapWriter::create(&path).expect("create"); + writer.write_frame(&beacon).expect("write beacon"); + writer.write_frame(&probe).expect("write probe"); + writer.flush().expect("flush"); + } + + let mut reader = PcapReader::open(&path).expect("open"); + assert!(reader.supports_80211()); + + let f1 = reader + .next_frame() + .expect("read 1") + .expect("frame 1 presente"); + assert_eq!(f1.data, beacon); + + let f2 = reader + .next_frame() + .expect("read 2") + .expect("frame 2 presente"); + assert_eq!(f2.data, probe); + + assert!(reader.next_frame().expect("read 3").is_none()); +} + +#[test] +fn analyze_pcap_finds_networks_and_handshake() { + let dir = tempfile::tempdir().expect("tempdir"); + let pcap_path = dir.path().join("sessao.pcap"); + let audit_path = dir.path().join("audit.log"); + + let beacon = common::radiotap_wrap( + &common::beacon_frame(common::ap(), "PcapHandshake", 3, &[2]), + 2422, + -38, + ); + let (f1, f2) = common::handshake_radiotap_frames("PcapHandshake", "senha-pcap", 2, false); + + { + let mut writer = PcapWriter::create(&pcap_path).expect("create"); + writer.write_frame(&beacon).expect("write beacon"); + writer.write_frame(&f1).expect("write msg1"); + writer.write_frame(&f2).expect("write msg2"); + writer.flush().expect("flush"); + } + + let engine = Engine::with_audit_path(&audit_path); + let snapshot = engine.analyze_pcap(&pcap_path).expect("analyze_pcap"); + + assert_eq!(snapshot.access_points.len(), 1); + assert_eq!( + snapshot.access_points[0].ssid.as_deref(), + Some("PcapHandshake") + ); + + assert_eq!(snapshot.handshakes.len(), 1); + let hs = &snapshot.handshakes[0]; + assert_eq!(hs.ssid.as_deref(), Some("PcapHandshake")); + assert!(hs.complete); + assert!(hs.crackable); +} diff --git a/crates/sabia-core/tests/scan_e2e.rs b/crates/sabia-core/tests/scan_e2e.rs new file mode 100644 index 0000000..478a569 --- /dev/null +++ b/crates/sabia-core/tests/scan_e2e.rs @@ -0,0 +1,87 @@ +//! Integração: beacon + probe + handshake completo passando pelo caminho +//! público de agregação (`ScanTable`), verificando que AP, cliente e +//! handshake são detectados de ponta a ponta. + +mod common; + +use sabia_core::scan::ScanTable; + +#[test] +fn detects_ap_client_and_handshake_end_to_end() { + let mut table = ScanTable::new(); + + // RSN com AKM PSK (2) para que a rede apareça como WPA2-PSK. + let beacon = common::beacon_frame(common::ap(), "RedeTeste", 6, &[2]); + table.ingest(&common::radiotap_wrap(&beacon, 2437, -35)); + + let probe = common::probe_request(common::sta(), "RedeTeste"); + table.ingest(&common::radiotap_wrap(&probe, 2437, -50)); + + let (f1, f2) = common::handshake_radiotap_frames("RedeTeste", "senha-correta", 2, false); + let ev1 = table.ingest_at(&f1, 10); + assert!(ev1.eapol, "quadro msg1 deveria ser reconhecido como EAPOL"); + let ev2 = table.ingest_at(&f2, 11); + assert!(ev2.eapol, "quadro msg2 deveria ser reconhecido como EAPOL"); + assert!( + ev2.handshake_updated, + "msg2 deveria completar/atualizar o handshake" + ); + + let aps = table.access_points(); + assert_eq!(aps.len(), 1); + let ap = &aps[0]; + assert_eq!(ap.bssid, sabia_core::dot11::mac_to_string(&common::ap())); + assert_eq!(ap.ssid.as_deref(), Some("RedeTeste")); + assert_eq!(ap.channel, Some(6)); + assert_eq!(ap.security.as_deref(), Some("WPA2")); + + let clients = table.clients(); + assert!(clients + .iter() + .any(|c| c.mac == sabia_core::dot11::mac_to_string(&common::sta()))); + + let handshakes = table.handshakes(); + assert_eq!(handshakes.len(), 1); + let hs = &handshakes[0]; + assert_eq!(hs.ssid.as_deref(), Some("RedeTeste")); + assert!(hs.complete, "handshake deveria estar completo (msg1+msg2)"); + assert!(hs.crackable, "handshake deveria ser quebrável"); + + let stats = table.stats(); + assert_eq!(stats.aps, 1); + assert_eq!(stats.clients, 1); + assert_eq!(stats.handshakes, 1); + assert!(stats.eapol_frames >= 2); +} + +#[test] +fn hidden_ssid_beacon_is_marked_hidden() { + let mut table = ScanTable::new(); + + // SSID vazio = rede oculta. + let beacon = common::beacon_frame(common::ap(), "", 11, &[2]); + table.ingest(&common::radiotap_wrap(&beacon, 2462, -60)); + + let aps = table.access_points(); + assert_eq!(aps.len(), 1); + assert!(aps[0].hidden); +} + +#[test] +fn pmkid_only_handshake_is_detected_and_crackable() { + let mut table = ScanTable::new(); + + // Beacon com SSID visível, para que o handshake herde o SSID via hint. + let beacon = common::beacon_frame(common::ap(), "RedePmkid", 11, &[2]); + table.ingest_at(&common::radiotap_wrap(&beacon, 2462, -60), 1); + + // Handshake via PMKID (só msg1, sem msg2/snonce). + let (m1, _m2) = common::handshake_frames("RedePmkid", "outra-senha", 2, true); + let f1 = common::frame_with_eapol(true, common::ap(), common::sta(), common::sta(), &m1); + let ev = table.ingest_at(&common::radiotap_wrap(&f1, 2462, -55), 20); + assert!(ev.eapol); + + let handshakes = table.handshakes(); + assert_eq!(handshakes.len(), 1); + assert!(handshakes[0].crackable, "PMKID isolado já é quebrável"); +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..dd6cd5f --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,40 @@ +# Architecture + +**English** · [Português](pt-BR/arquitetura.md) + +Two crates and one app: + +``` +crates/sabia-core all the logic (library) +crates/sabia-cli the `sabia` binary (CLI + local UI server) +apps/gui interface (ui/ = HTML/CSS/JS; src-tauri/ = desktop app) +``` + +The CLI and the desktop app are thin clients: both call the core's `Engine`. The browser UI (`sabia ui`) and the Tauri one load the same `apps/gui/ui/`. + +## `sabia-core` + +| Module | Role | +|---|---| +| `engine.rs` | Orchestrates everything and exposes the API the CLI and GUI consume. | +| `iface.rs`, `netlink.rs` | Discovers interfaces and channels; toggles monitor mode via netlink/`iw`. | +| `radiotap.rs` | Radiotap header parsing; band and channel. | +| `dot11.rs` | 802.11 frame parsing (management: beacon, probe). | +| `eapol.rs` | EAPOL and 4-way handshake field parsing. | +| `handshake.rs` | Assembles the handshake (ANonce, SNonce, MIC, PMKID) from frames. | +| `crack.rs` | Derives PMK (PBKDF2-HMAC-SHA1) and PTK, checks MIC and PMKID. | +| `scan.rs` | Hops channels and aggregates APs and clients seen. | +| `capture.rs`, `inject.rs` | Raw AF_PACKET capture; frame injection (deauth). | +| `pcapreader.rs`, `pcapwriter.rs` | `.pcap` read and write. | +| `audit.rs` | Local hash-chained audit trail. | +| `doctor.rs` | Diagnostics of privileges, tools and interfaces. | +| `update.rs` | Self-update from GitHub releases (curl + SHA-256). | +| `error.rs`, `testutil.rs` | Shared error type; test helpers. | + +## Platform + +Capture and monitor mode are **Linux** (`AF_PACKET` + netlink). Parsing (`radiotap`, `dot11`, `eapol`, `handshake`), cracking (`crack`) and `.pcap` are cross-platform, being offline — which is why the tests run on Linux, Windows and macOS in CI. + +## UI transport + +`sabia ui` starts a server on `127.0.0.1` protected by a token generated on the spot; it serves `index.html`/`app.css`/`app.js`/`sabia.png` embedded in the binary plus a JSON API. The Tauri app uses the same files, calling the `Engine` directly via Tauri commands instead of HTTP. diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..fe2e637 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,83 @@ +# Terminal (`sabia`) + +**English** · [Português](pt-BR/cli.md) + +The `sabia` binary has the global option `--audit ` (audit trail; default `.sabia-audit.log`) and the commands below. Capture and monitor mode require Linux and root; analysis and cracking run on any platform. + +## `iface` — interfaces and monitor mode + +```bash +sabia iface list # Wi-Fi interfaces and their modes +sabia iface channels # channels the radio supports [--json] +sudo sabia iface monitor on # enable monitor mode (on|off) +``` + +## `scan` — capture the air + +```bash +sudo sabia scan [options] +``` + +| Option | Effect | +|---|---| +| `--channel ` | Fix a channel (without it, hops across the supported ones). | +| `--band <2.4\|5\|6\|all>` | Band when hopping channels. | +| `--hop-ms ` | Time on each channel (default 350). | +| `--seconds ` | Stop after N seconds (default: until Ctrl+C). | +| `--out ` | Save captured frames. | +| `--crack ` | After capturing, try to crack the first handshake. | +| `--json` | Print only the final JSON. | + +Shows networks (APs), clients and handshakes seen (4-way and PMKID). + +## `analyze` — read a `.pcap` offline + +```bash +sabia analyze [--json] +``` + +## `crack` — dictionary cracking + +```bash +sabia crack --wordlist [--bssid ] [--station ] [--json] +``` + +Recomputes the PMK with PBKDF2-HMAC-SHA1 and checks the MIC (4-way) or the PMKID, for WPA/WPA2-PSK. `--bssid`/`--station` filter which handshake to use. + +## `deauth` — authorized deauthentication test + +```bash +sudo sabia deauth --bssid [--station ] [--count 5] [--reason 7] --yes +``` + +Without `--station`, the target is broadcast. The **`--yes`** flag confirms you are authorized to test this network — without it, the command refuses. It is recorded in the audit trail. + +## `doctor` — diagnostics + +```bash +sabia doctor [--json] +``` + +Checks privileges, tools (`iw`, `ip`) and monitor-capable interfaces. + +## `audit` — audit trail + +```bash +sabia audit verify [file] # check the hash-chain integrity +sabia audit show [--limit 20] # latest entries +``` + +## `update` — updating + +```bash +sabia update # check for a new version +sabia update --apply # download, verify the SHA-256 and swap the executable +``` + +## `ui` — desktop/web interface + +```bash +sabia ui [--port 4337] [--no-open] +``` + +Starts the local interface server and opens the browser (or not, with `--no-open`). diff --git a/docs/interface.md b/docs/interface.md new file mode 100644 index 0000000..3f56ea1 --- /dev/null +++ b/docs/interface.md @@ -0,0 +1,20 @@ +# Desktop app + +**English** · [Português](pt-BR/interface.md) + +The desktop app (Tauri) and the browser version (`sabia ui`) are the same front-end, in `apps/gui/ui/`. Only the transport differs: the desktop app talks to the core through Tauri; in the browser, `sabia-cli` starts a local server on `127.0.0.1` protected by a token generated on the spot. + +## Screens + +- **Interface sidebar:** lists the Wi-Fi interfaces; pick one to work with. The footer has the version and the one-click **update** button. +- **Capture panel:** header with the current mode (managed/monitor), an **animated signal line**, and the **enable monitor mode** and **start capture** buttons. +- **Authorized-use notice**, always in view: use only on your own networks or with authorization; every monitor activation and capture goes into the local audit trail. +- **Networks seen:** live table with SSID, signal bars, channel and a color-coded security badge (open, WEP, WPA/WPA2, WPA3). + +## Privilege + +Monitor mode and capture require root/admin. On startup, the app elevates automatically (on Linux and macOS it restarts elevated; on Windows, via the manifest). A notice appears while elevation is requested, with a retry option. + +## Themes and language + +**Auto, Light and Dark** themes and interface in **Portuguese and English**. The system language is detected at start; the choice is saved. The source text is Portuguese and English is applied at runtime (see `apps/gui/ui/i18n.js`). diff --git a/docs/pt-BR/arquitetura.md b/docs/pt-BR/arquitetura.md new file mode 100644 index 0000000..942c9bc --- /dev/null +++ b/docs/pt-BR/arquitetura.md @@ -0,0 +1,40 @@ +# Arquitetura + +[English](../architecture.md) · **Português** + +Dois crates e um app: + +``` +crates/sabia-core toda a lógica (biblioteca) +crates/sabia-cli binário `sabia` (CLI + servidor local da UI) +apps/gui interface (ui/ = HTML/CSS/JS; src-tauri/ = app desktop) +``` + +A CLI e o app desktop são clientes finos: ambos chamam o `Engine` do núcleo. A UI do navegador (`sabia ui`) e a do Tauri carregam o mesmo `apps/gui/ui/`. + +## `sabia-core` + +| Módulo | Papel | +|---|---| +| `engine.rs` | Orquestra tudo e expõe a API que CLI e GUI consomem. | +| `iface.rs`, `netlink.rs` | Descobre interfaces e canais; liga/desliga modo monitor por netlink/`iw`. | +| `radiotap.rs` | Parsing do cabeçalho radiotap; banda e canal. | +| `dot11.rs` | Parsing de quadros 802.11 (gerenciamento: beacon, probe). | +| `eapol.rs` | Parsing do EAPOL e dos campos do 4-way handshake. | +| `handshake.rs` | Monta o handshake (ANonce, SNonce, MIC, PMKID) a partir dos quadros. | +| `crack.rs` | Deriva PMK (PBKDF2-HMAC-SHA1) e PTK, confere MIC e PMKID. | +| `scan.rs` | Percorre canais e agrega APs e clientes vistos. | +| `capture.rs`, `inject.rs` | Captura crua por AF_PACKET; injeção de quadros (deauth). | +| `pcapreader.rs`, `pcapwriter.rs` | Leitura e escrita de `.pcap`. | +| `audit.rs` | Trilha de auditoria local encadeada por hash. | +| `doctor.rs` | Diagnóstico de privilégios, ferramentas e interfaces. | +| `update.rs` | Auto-atualização por releases do GitHub (curl + SHA-256). | +| `error.rs`, `testutil.rs` | Tipo de erro compartilhado; ajudantes de teste. | + +## Plataforma + +Captura e modo monitor são **Linux** (`AF_PACKET` + netlink). Parsing (`radiotap`, `dot11`, `eapol`, `handshake`), quebra (`crack`) e `.pcap` são multiplataforma, por serem offline — por isso os testes rodam em Linux, Windows e macOS no CI. + +## Transporte da UI + +`sabia ui` sobe um servidor em `127.0.0.1` protegido por um token gerado na hora; serve `index.html`/`app.css`/`app.js`/`sabia.png` embutidos no binário e uma API JSON. O app Tauri usa os mesmos arquivos, chamando o `Engine` direto por comandos Tauri em vez de HTTP. diff --git a/docs/pt-BR/cli.md b/docs/pt-BR/cli.md new file mode 100644 index 0000000..09bd945 --- /dev/null +++ b/docs/pt-BR/cli.md @@ -0,0 +1,83 @@ +# Terminal (`sabia`) + +[English](../cli.md) · **Português** + +O binário `sabia` tem a opção global `--audit ` (trilha de auditoria; padrão `.sabia-audit.log`) e os comandos abaixo. Captura e modo monitor exigem Linux e root; análise e quebra rodam em qualquer plataforma. + +## `iface` — interfaces e modo monitor + +```bash +sabia iface list # interfaces Wi-Fi e seus modos +sabia iface channels # canais que o rádio suporta [--json] +sudo sabia iface monitor on # liga o modo monitor (on|off) +``` + +## `scan` — captura o ar + +```bash +sudo sabia scan [opções] +``` + +| Opção | Efeito | +|---|---| +| `--channel ` | Fixa um canal (sem isso, percorre os suportados). | +| `--band <2.4\|5\|6\|all>` | Banda ao percorrer canais. | +| `--hop-ms ` | Tempo em cada canal (padrão 350). | +| `--seconds ` | Para depois de N segundos (padrão: até Ctrl+C). | +| `--out ` | Salva os quadros capturados. | +| `--crack ` | Depois de capturar, tenta quebrar o primeiro handshake. | +| `--json` | Mostra apenas o JSON final. | + +Mostra redes (APs), clientes e handshakes vistos (4-way e PMKID). + +## `analyze` — lê um `.pcap` offline + +```bash +sabia analyze [--json] +``` + +## `crack` — quebra por dicionário + +```bash +sabia crack --wordlist [--bssid ] [--station ] [--json] +``` + +Recalcula a PMK com PBKDF2-HMAC-SHA1 e confere o MIC (4-way) ou o PMKID, para WPA/WPA2-PSK. `--bssid`/`--station` filtram qual handshake usar. + +## `deauth` — teste autorizado de desautenticação + +```bash +sudo sabia deauth --bssid [--station ] [--count 5] [--reason 7] --yes +``` + +Sem `--station`, o destino é broadcast. A flag **`--yes`** confirma que você tem autorização para testar esta rede — sem ela, o comando recusa. Fica na trilha de auditoria. + +## `doctor` — diagnóstico + +```bash +sabia doctor [--json] +``` + +Confere privilégios, ferramentas (`iw`, `ip`) e interfaces com suporte a modo monitor. + +## `audit` — trilha de auditoria + +```bash +sabia audit verify [arquivo] # confere a integridade da cadeia por hash +sabia audit show [--limit 20] # últimas entradas +``` + +## `update` — atualização + +```bash +sabia update # verifica se há versão nova +sabia update --apply # baixa, confere o SHA-256 e troca o executável +``` + +## `ui` — interface gráfica + +```bash +sabia ui [--port 4337] [--no-open] +``` + +Sobe o servidor local da interface e abre o navegador (ou não, com `--no-open`). diff --git a/docs/pt-BR/interface.md b/docs/pt-BR/interface.md new file mode 100644 index 0000000..198b500 --- /dev/null +++ b/docs/pt-BR/interface.md @@ -0,0 +1,20 @@ +# Interface gráfica + +[English](../interface.md) · **Português** + +O app desktop (Tauri) e a versão no navegador (`sabia ui`) são o mesmo front-end, em `apps/gui/ui/`. Muda só o transporte: o app desktop fala com o núcleo pelo Tauri; no navegador, o `sabia-cli` sobe um servidor local em `127.0.0.1` protegido por um token gerado na hora. + +## Telas + +- **Sidebar de interfaces:** lista as interfaces Wi-Fi; selecione uma para trabalhar. No rodapé ficam a versão e o botão de **atualização** em um clique. +- **Painel de captura:** cabeçalho com o modo atual (managed/monitor), uma **linha de sinal animada**, e os botões **ativar modo monitor** e **iniciar captura**. +- **Aviso de uso autorizado**, sempre à vista: use só em redes próprias ou com autorização; toda ativação de monitor e captura entra na trilha de auditoria local. +- **Redes vistas:** tabela em tempo real com SSID, barras de sinal, canal e um selo de segurança por cor (aberta, WEP, WPA/WPA2, WPA3). + +## Privilégio + +Modo monitor e captura exigem root/admin. Ao abrir, o app pede elevação automaticamente (no Linux e macOS ele reinicia elevado; no Windows, pelo manifesto). Um aviso aparece enquanto a elevação é pedida, com a opção de tentar de novo. + +## Temas e idioma + +Temas **Auto, Claro e Escuro** e interface em **português e inglês**. O idioma do sistema é detectado no início; a escolha fica salva. O texto-fonte é em português e o inglês é aplicado em tempo de execução (veja `apps/gui/ui/i18n.js`). diff --git a/scripts/apt-install.sh b/scripts/apt-install.sh new file mode 100755 index 0000000..db005d3 --- /dev/null +++ b/scripts/apt-install.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Instala pacotes do apt no CI sem depender do espelho estar rápido: usa os .deb em cache quando +# existem e, se o apt travar ou falhar, tenta de novo com tempo limite por tentativa. +# +# Os passos de apt do CI já levaram de 13 a 28 minutos num dia de espelho lento, contra uns 35 s +# no normal. O cache evita o download; o tempo limite evita ficar preso. +# +# Uso: scripts/apt-install.sh ... +set -euo pipefail +[ $# -ge 2 ] || { echo "uso: $0 ..." >&2; exit 2; } +cache="$1"; shift +pkgs=("$@") +mkdir -p "$cache/partial" +# O apt roda como root e deixa arquivos (lock, .deb) que o usuário do runner não lê; sem devolver a +# posse, o actions/cache falha ao empacotar a pasta ("tar failed with exit code 2") e nunca grava. +trap 'sudo chown -R "$(id -u):$(id -g)" "$cache" 2>/dev/null || true; rm -f "$cache/lock"' EXIT +opts=(-o "Dir::Cache::archives=$cache" -y) + +# 1) com .deb em cache: instala direto, sem nem atualizar o índice do apt +if ls "$cache"/*.deb >/dev/null 2>&1 && timeout 300 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then + echo "instalado a partir do cache" + exit 0 +fi + +# 2) sem cache (ou incompleto): atualiza e instala, até 3 tentativas com tempo limite +for tentativa in 1 2 3; do + if timeout 240 sudo apt-get update && timeout 600 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then + echo "instalado (tentativa $tentativa)" + exit 0 + fi + echo "apt falhou ou passou do tempo (tentativa $tentativa de 3)" >&2 + sleep $((tentativa * 10)) +done +echo "não foi possível instalar: ${pkgs[*]}" >&2 +exit 1 diff --git a/scripts/check-ui-i18n.mjs b/scripts/check-ui-i18n.mjs new file mode 100644 index 0000000..5ace049 --- /dev/null +++ b/scripts/check-ui-i18n.mjs @@ -0,0 +1,293 @@ +#!/usr/bin/env node +// Confere se o dicionário inglês cobre os textos da interface. +// +// A varredura é estática e sem dependências: lê os arquivos da interface +// (JS e HTML) e as mensagens pt-BR que o núcleo em Rust manda pra tela +// (erros, diagnóstico, status do engine), e falha quando encontra uma +// string que parece português e não tem tradução. Também recusa chaves +// duplicadas no dicionário (num objeto JS a última vence em silêncio). +// +// Uso: node scripts/check-ui-i18n.mjs + +import { readFileSync, readdirSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(fileURLToPath(new URL('.', import.meta.url)), '..'); +const rel = (p) => relative(root, p).replaceAll('\\', '/'); + +// ---------------------------------------------------------------- dicionário + +const dictPath = join(root, 'apps/gui/ui/i18n-en.js'); +const dictSource = readFileSync(dictPath, 'utf8'); + +const keyRe = /^\s*(['"])((?:(?!\1).)*)\1\s*:/gm; +const seen = new Map(); +const duplicates = []; +let m; +while ((m = keyRe.exec(dictSource))) { + const key = m[2]; + const line = dictSource.slice(0, m.index).split('\n').length; + if (seen.has(key)) duplicates.push({ key, first: seen.get(key), again: line }); + else seen.set(key, line); +} + +if (duplicates.length) { + console.error('Chaves duplicadas em apps/gui/ui/i18n-en.js:'); + for (const d of duplicates) console.error(` ${JSON.stringify(d.key)} (linhas ${d.first} e ${d.again})`); + process.exitCode = 1; +} + +const { EN } = await import(new URL('../apps/gui/ui/i18n-en.js', import.meta.url).href); + +const exact = new Set(Object.keys(EN).filter((k) => !k.includes('{n}'))); +const patterns = Object.keys(EN) + .filter((k) => k.includes('{n}')) + .map((k) => { + const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); + return new RegExp('^' + parts.join('(.+?)') + '$', 's'); + }); + +function translated(text) { + if (exact.has(text)) return true; + return patterns.some((re) => re.test(text)); +} + +// ---------------------------------------------------------------- heurística + +const ACCENT = /[áàâãäéèêëíìîïóòôõöúùûüçÁÀÂÃÄÉÈÊËÍÌÎÏÓÒÔÕÖÚÙÛÜÇ]/; +const WORDS = [ + 'rede', 'redes', 'interface', 'interfaces', 'captura', 'capturar', 'modo', 'ativar', 'ativa', + 'desativar', 'iniciar', 'parar', 'nenhum', 'nenhuma', 'selecione', 'selecionada', 'canal', + 'sinal', 'segurança', 'clientes', 'cliente', 'oculta', 'oculto', 'privilégio', 'administrador', + 'necessário', 'pra', 'própria', 'próprias', 'autorização', 'responsável', 'sessão', 'trilha', + 'auditoria', 'registrada', 'registrado', 'local', 'adaptador', 'compatível', 'recarregue', + 'escutando', 'levar', 'segundos', 'enquanto', 'canais', 'varridos', 'erro', 'desconhecido', + 'desconhecida', 'não', 'sim', 'mais', 'menos', 'uma', 'uns', 'umas', 'aqui', 'onde', 'quando', + 'como', 'qual', 'para', 'com', 'sem', 'dos', 'das', 'por', 'que', 'checando', 'checar', + 'atualização', 'atualizado', 'trocar', 'possível', 'quebrável', 'quebrado', 'rompida', + 'corrompida', 'inválido', 'inválida', 'entrada', 'dicionário', 'quadros', 'quantidade', + 'injetar', 'encontrado', 'encontrada', 'suportada', 'permissão', 'insuficiente', 'instale', + 'root', 'indisponível', 'plataforma', 'criada', 'ainda', +]; + +const BRAND = /Sabiá|Português|English/gi; +const isPortuguese = (raw) => { + const s = raw.replace(BRAND, ' '); + return ACCENT.test(s) || WORDS.some((w) => new RegExp(`(^|[^a-zà-ú])${w}([^a-zà-ú]|$)`, 'i').test(s)); +}; + +const ALLOW = new Set([ + 'Sabiá', 'Português', 'English', 'Auto', 'Wi-Fi', 'SSID', 'BSSID', 'MAC', 'AP', 'WPA', 'WPA2', + 'WPA3', 'WEP', 'PMKID', 'EAPOL', 'PMK', 'PTK', 'pcap', 'radiotap', '802.11', 'dBm', 'GHz', + 'MHz', 'RSSI', 'CAP_NET_ADMIN', 'CAP_NET_RAW', 'AF_PACKET', 'OK', +]); +const ALLOW_PREFIX = ['http://', 'https://', 'data:', 'file:', '#', '.', '/']; +const ALLOW_EXACT = new Set(['—', '-', '·']); + +function suspicious(text) { + const s = text.replace(/\s+/g, ' ').trim(); + if (!s || s.length < 2) return false; + if (ALLOW.has(s) || ALLOW_EXACT.has(s)) return false; + if (ALLOW_PREFIX.some((p) => s.startsWith(p))) return false; + if (!isPortuguese(s)) return false; + // técnico: seletores, classes, extensões, expressões + if (/^[.#\w-]+(\s*[,>+~]\s*[.#\w-]+)+$/.test(s)) return false; + if (/^[a-z0-9_.:/@%{}$-]+$/i.test(s) && !ACCENT.test(s)) return false; + return true; +} + +// ---------------------------------------------------------------- extratores + +function jsStrings(source, file, baseLine = 1) { + const out = []; + let i = 0; + let line = baseLine; + let prev = ''; + const skipRegex = () => { + i++; + let inClass = false; + while (i < source.length) { + const ch = source[i]; + if (ch === '\\') { i += 2; continue; } + if (ch === '[') inClass = true; + else if (ch === ']') inClass = false; + else if (ch === '/' && !inClass) { i++; break; } + else if (ch === '\n') line++; + i++; + } + while (i < source.length && /[a-z]/i.test(source[i])) i++; + }; + while (i < source.length) { + const c = source[i]; + if (c === '\n') { line++; i++; prev = '\n'; continue; } + if (c === ' ' || c === '\t' || c === '\r') { i++; continue; } + if (c === '/' && source[i + 1] === '/') { + while (i < source.length && source[i] !== '\n') i++; + continue; + } + if (c === '/' && source[i + 1] === '*') { + i += 2; + while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) { if (source[i] === '\n') line++; i++; } + i += 2; + continue; + } + if (c === '/' && (/[=(:,[!&|?{};+\-*%^~<>]/.test(prev) || prev === 'return')) { + skipRegex(); + prev = ')'; + continue; + } + if (c === "'" || c === '"') { + const quote = c; + const startLine = line; + let value = ''; + i++; + while (i < source.length) { + const ch = source[i]; + if (ch === '\\') { + const n = source[i + 1]; + if (n === 'n') value += '\n'; + else if (n === 't') value += '\t'; + else if (n === 'r') value += '\r'; + else if (n === '\n') line++; + else value += n ?? ''; + i += 2; + continue; + } + if (ch === quote) { i++; break; } + if (ch === '\n') line++; + value += ch; + i++; + } + out.push({ text: value, file, line: startLine }); + prev = ')'; + continue; + } + if (c === '`') { + const startLine = line; + let value = ''; + i++; + while (i < source.length) { + const ch = source[i]; + if (ch === '\\') { + const n = source[i + 1]; + if (n === 'n') value += '\n'; + else if (n === 't') value += '\t'; + else if (n === '\n') line++; + else value += n ?? ''; + i += 2; + continue; + } + if (ch === '`') { i++; break; } + if (ch === '$' && source[i + 1] === '{') { + value += '{n}'; + i += 2; + const exprStart = i; + const exprLine = line; + let depth = 1; + while (i < source.length && depth > 0) { + if (source[i] === '{') depth++; + else if (source[i] === '}') depth--; + else if (source[i] === '\n') line++; + i++; + } + out.push(...jsStrings(source.slice(exprStart, i - 1), file, exprLine)); + continue; + } + if (ch === '\n') line++; + value += ch; + i++; + } + out.push({ text: value, file, line: startLine }); + prev = ')'; + continue; + } + prev = c; + i++; + } + return out; +} + +function htmlStrings(source, file, baseLine = 1) { + const out = []; + const lineAt = (idx) => baseLine + source.slice(0, idx).split('\n').length - 1; + // todo texto entre tags (inclusive começo e fim) — nós de texto reais + let last = 0; + for (const m of source.matchAll(/<[^>]*>/g)) { + out.push({ text: source.slice(last, m.index), file, line: lineAt(last) }); + last = m.index + m[0].length; + } + out.push({ text: source.slice(last), file, line: lineAt(last) }); + // atributos traduzíveis (com aspas simples ou duplas) + for (const m of source.matchAll(/\b(placeholder|title|aria-label|alt|data-empty)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) { + out.push({ text: m[2] ?? m[3], file, line: lineAt(m.index) }); + } + return out; +} + +// Extrai mensagens pt-BR do núcleo em Rust: #[error("...")], .push("...".into()), +// "...".into() e format!("...", ...). Placeholders ({name}, {0}, {} etc.) são +// normalizados para {n}, igual ao que o dicionário usa. +function rustLabels(source, file) { + const out = []; + const lineAt = (idx) => source.slice(0, idx).split('\n').length; + const norm = (s) => s.replace(/\{[^}]*\}/g, '{n}'); + const grab = (re) => { + for (const m of source.matchAll(re)) { + out.push({ text: norm(m[1]), file, line: lineAt(m.index) }); + } + }; + grab(/#\[error\(\s*"((?:[^"\\]|\\.)*)"\s*\)\]/g); + grab(/"((?:[^"\\]|\\.)*)"\s*\.into\(\)/g); + grab(/format!\(\s*"((?:[^"\\]|\\.)*)"/g); + return out; +} + +// ---------------------------------------------------------------- coleta + +const uiFiles = readdirSync(join(root, 'apps/gui/ui')) + .filter((f) => f.endsWith('.js') && f !== 'i18n-en.js') + .map((f) => join(root, 'apps/gui/ui', f)); +uiFiles.push(join(root, 'apps/gui/ui/index.html')); + +const rustFiles = [ + 'crates/sabia-core/src/error.rs', + 'crates/sabia-core/src/doctor.rs', + 'crates/sabia-core/src/engine.rs', +].map((f) => join(root, f)); + +const found = []; +for (const file of uiFiles) { + const source = readFileSync(file, 'utf8'); + if (file.endsWith('.html')) found.push(...htmlStrings(source, file)); + else { + for (const str of jsStrings(source, file)) { + if (/<[a-z!/][^>]*>/i.test(str.text)) found.push(...htmlStrings(str.text, file, str.line)); + else found.push(str); + } + } +} +for (const file of rustFiles) found.push(...rustLabels(readFileSync(file, 'utf8'), file)); + +const missing = []; +const seenMissing = new Set(); +for (const { text, file, line } of found) { + if (!suspicious(text)) continue; + const key = text.replace(/\s+/g, ' ').trim(); + if (translated(key)) continue; + const id = `${key}\u0000${rel(file)}\u0000${line}`; + if (seenMissing.has(id)) continue; + seenMissing.add(id); + missing.push({ text: key, file: rel(file), line }); +} + +if (missing.length) { + console.error('Textos em português sem tradução em apps/gui/ui/i18n-en.js:\n'); + for (const x of missing) console.error(` ${x.file}:${x.line} ${JSON.stringify(x.text)}`); + console.error(`\n${missing.length} texto(s) sem tradução.`); + process.exitCode = 1; +} + +if (!process.exitCode) { + console.log(`i18n ok: ${found.length} textos verificados, ${Object.keys(EN).length} chaves no dicionário.`); +}