24 Commits
Author SHA1 Message Date
Lucas RayanandClaude Sonnet 5 ba0c99d5b6 chore(release): bump version to 1.0.4
CI / Dependency audit (push) Canceled after 0s
CI / Check desktop app (Linux) (push) Canceled after 0s
Release / Create release (push) Successful in 6s
CI / Format and clippy (push) Canceled after 0s
CI / Tests (Linux) (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Successful in 53s
Release / Desktop app (linux-aarch64) (push) Successful in 4m45s
Release / CLI (linux-x86_64) (push) Successful in 1m28s
Release / Desktop app (linux-x86_64) (push) Successful in 10m26s
Release / Desktop app (windows) (push) Successful in 8m12s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 22:06:19 -03:00
Lucas RayanandClaude Sonnet 5 476e27e253 ci(release): build linux-x86_64 natively on the new amd64 runner
A native x86_64 Gitea Actions runner (hostinger-runner, label
linux-amd64) is now available alongside the two aarch64 runners
(oracle-runner/-b, label linux-arm64). Routes the linux-x86_64 leg of
the cli and gui release matrices to it instead of cross-compiling it
from an aarch64 host, same change as the Guará repository.

- matrix.include gets an explicit runner field per platform
  (linux-arm64 for aarch64, linux-amd64 for x86_64 and windows);
  runs-on: ${{ matrix.runner }} picks it per job. windows is pinned to
  linux-amd64 explicitly rather than relying on the ubuntu-latest
  alias, since that alias has already been reassigned once between
  the aarch64 and the x86_64 runner on this Gitea instance.
- The "Platform cross-linker" step (gcc-x86-64-linux-gnu) only runs
  when NOT on the native amd64 runner, since a native build needs no
  cross-linker.
- scripts/build-gui.sh and scripts/package-cli.sh are intentionally
  left untouched: both already branch on `uname -m` and take the
  simple native path when run on x86_64, so no script change was
  needed. The existing cross-compile path (qemu wrappers, the ELF
  launcher that survives tauri-bundler's dd patch) stays in place as a
  dormant fallback in case the single amd64 runner is ever
  unavailable — deliberately kept, not deleted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 22:06:13 -03:00
Lucas RayanandClaude Sonnet 5 55e19a8e5b chore(i18n): finish translating code comments and scripts to English
CI / Format and clippy (push) Successful in 1m13s
CI / Dependency audit (push) Successful in 13s
CI / Tests (Linux) (push) Successful in 1m35s
Release / Create release (push) Successful in 7s
CI / Check desktop app (Linux) (push) Successful in 3m19s
Release / CLI (linux-aarch64) (push) Successful in 1m3s
Release / CLI (linux-x86_64) (push) Successful in 1m28s
Release / Desktop app (linux-aarch64) (push) Successful in 7m16s
Release / Desktop app (linux-x86_64) (push) Successful in 10m17s
Release / Desktop app (windows) (push) Successful in 4m54s
Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:44:39 -03:00
Lucas RayanandClaude Sonnet 5 bf059fd0bc docs: add desktop app screenshots
Sabiá had no screenshots at all before this, just the logo. Add
networks/attack/settings screenshots (light/dark, en/pt-BR) captured
from the real UI with synthetic audit fixtures (7 mock access points
across 2.4/5 GHz covering open/WEP/WPA/WPA2/WPA3/802.1X-Enterprise, mock
client stations, one crackable handshake — no real third-party network
data), and reference them from README.md, README.pt-BR.md,
docs/interface.md and docs/pt-BR/interface.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:44:29 -03:00
Lucas RayanandClaude Sonnet 5 a904723f96 fix(gui): add non-Linux stubs for pmkid_hunt/pmf_test and fix Windows warnings
main.rs failed to compile on Windows: pmkid_hunt and pmf_test were only
implemented under #[cfg(target_os = "linux")], with no fallback stub,
so the Tauri commands calling them did not resolve.

- Add #[cfg(not(target_os = "linux"))] stubs for both, matching the
  existing pattern for every other Linux-only Engine method (returns
  SabiaError::UnsupportedPlatform).
- Mark the DeauthJob/Engine fields and session_ap/client_snapshot
  helpers that only the Linux-only code paths use with
  #[cfg_attr(not(target_os = "linux"), allow(dead_code))], same for
  eviltwin.rs's form_value/url_decode (only called from the
  #[cfg(target_os = "linux")] HTTP handler) — silences the 5
  dead_code warnings a Windows build produced.
- .setup(|app| ...) used app only inside unix/linux/macos-gated
  blocks, so it was unused on Windows; renamed the closure parameter
  and reintroduce it under the matching cfg.
- Add the missing English dictionary entry for the mutation-rules
  checkbox label in app.js (found by visually reviewing a generated
  screenshot: it was rendering in Portuguese in the English UI even
  though check-ui-i18n.mjs reported full coverage).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:44:17 -03:00
Lucas RayanandClaude Sonnet 5 2dfbbfc64a fix(ci): stop dd from corrupting the x86_64 linuxdeploy launcher
Same root cause and fix as Guará: tauri-bundler's prepare_tools runs
'dd ... seek=8 conv=notrunc' on the cached linuxdeploy-x86_64 binary on
the aarch64 Gitea runner. A bash wrapper script there loses its shebang
to the dd and fails with ENOENT; a native ELF launcher survives because
bytes 8-10 of the ELF header are unused EI_PAD padding. Replace the
wrapper with a small ELF launcher compiled with cc, and cache
AppRun-x86_64 explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:44:06 -03:00
Lucas Rayan 659701b2d4 chore: padroniza arquitetura Ciberfauna, logo na raiz, hooks de verificação e CI multi-arch no runner ARM
CI / Auditoria de dependências (push) Successful in 12s
CI / Testes (Linux) (push) Successful in 1m34s
Release / Cria a release (push) Successful in 5s
Release / App desktop (linux-aarch64) (push) Successful in 7m32s
CI / Formatação e clippy (push) Successful in 1m9s
CI / Compila o app desktop (Linux) (push) Successful in 1m36s
Release / CLI (linux-aarch64) (push) Successful in 1m23s
Release / CLI (linux-x86_64) (push) Successful in 1m40s
Release / App desktop (linux-x86_64) (push) Failing after 9m5s
Release / App desktop (windows) (push) Failing after 6m20s
2026-10-30 17:38:58 -03:00
Lucas RayanandClaude Sonnet 5.5 30a086a808 release 1.0.3: correct version labels, Windows build fixed
Release / Cria a release (push) Successful in 8s
Release / CLI (linux-aarch64) (push) Successful in 48s
Release / CLI (linux-x86_64) (push) Successful in 1m2s
CI / Formatação e clippy (push) Successful in 20s
CI / Testes (Linux) (push) Successful in 23s
CI / Auditoria de dependências (push) Successful in 13s
CI / Compila o app desktop (Linux) (push) Successful in 1m18s
Release / App desktop (windows) (push) Failing after 3m27s
Release / App desktop (linux) (push) Successful in 5m4s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:41:00 -03:00
Lucas RayanandClaude Sonnet 5.5 833abc6308 fix: gate deauth_directional to Linux
CI / Formatação e clippy (push) Successful in 24s
CI / Testes (Linux) (push) Successful in 25s
CI / Auditoria de dependências (push) Canceled after 3s
CI / Compila o app desktop (Linux) (push) Canceled after 3s
The non-Linux stub at the bottom of the file duplicated a definition that had
no #[cfg(target_os = "linux")] of its own — it inherited none from the
unrelated #[allow] above it. Every non-Linux build (the desktop app's Windows
CI job included) failed with a duplicate-definition error and unresolved
Linux-only imports inside the always-compiled body.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:40:29 -03:00
Lucas RayanandClaude Sonnet 5.5 3fc3febff3 release 1.0.2: fix the desktop app build that v1.0.1 shipped broken
Release / App desktop (linux) (push) Successful in 4m48s
Release / CLI (linux-aarch64) (push) Successful in 1m4s
Release / CLI (linux-x86_64) (push) Successful in 1m6s
Release / App desktop (windows) (push) Failing after 3m7s
Release / Cria a release (push) Successful in 4s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:31:47 -03:00
Lucas RayanandClaude Sonnet 5.5 827ef0c2c0 ci: find the desktop bundles in CARGO_TARGET_DIR
CI / Formatação e clippy (push) Successful in 25s
CI / Testes (Linux) (push) Successful in 25s
CI / Auditoria de dependências (push) Successful in 10s
CI / Compila o app desktop (Linux) (push) Successful in 2m3s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 14:02:19 -03:00
Lucas RayanandClaude Sonnet 5.5 c7638a2f4e ci: build the desktop app (Linux and Windows) automatically on release
Release / Cria a release (push) Successful in 4s
Release / App desktop (linux) (push) Failing after 8m1s
Release / CLI (linux-aarch64) (push) Successful in 51s
Release / CLI (linux-x86_64) (push) Successful in 1m13s
Release / App desktop (windows) (push) Failing after 7m15s
Mirrors Guará's release workflow. No signing key needed here: Sabiá's updater
is the custom module in sabia-core, not the Tauri plugin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 13:42:35 -03:00
Lucas RayanandClaude Sonnet 5.5 d885d92643 release 1.0.1: update from Gitea, Linux-only CLI, desktop app for Linux and Windows
CI / Formatação e clippy (push) Successful in 49s
CI / Testes (Linux) (push) Successful in 49s
CI / Auditoria de dependências (push) Successful in 12s
CI / Compila o app desktop (Linux) (push) Successful in 1m12s
The updater reads the latest release from Gitea. update::apply refuses to
overwrite anything but the CLI. Release workflow builds the Linux CLI;
scripts/build-gui.sh builds the Linux and Windows desktop app. Docs say Linux
and Windows only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 13:17:32 -03:00
Lucas RayanandClaude Sonnet 5.5 0e31d28342 ci: create a Gitea release when a v* tag is pushed
CI / Formatação e clippy (push) Successful in 26s
CI / Testes (Linux) (push) Successful in 22s
CI / Compila o app desktop (Linux) (push) Successful in 4m23s
CI / Auditoria de dependências (push) Successful in 5m31s
Notes come from the matching CHANGELOG.md section. The macOS and Windows
binaries the old release workflow built need runners this Gitea does not have.
The CI also ignores .wolf, .claude and the release files.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 12:37:01 -03:00
Lucas RayanandClaude Sonnet 5.5 9bcdd6c139 ci: move the CI to Gitea Actions and cache what is rebuilt
CI / Testes (Linux) (push) Successful in 1m1s
CI / Formatação e clippy (push) Successful in 1m43s
CI / Auditoria de dependências (push) Successful in 5m19s
CI / Compila o app desktop (Linux) (push) Successful in 4m59s
Linux-only runner, so the Windows and macOS legs and GitHub's audit action are
gone. The Rust toolchain, cargo registry and target directories live on the
runner's persistent toolcache volume. release.yml stays under .github and is
not run by Gitea.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 10:45:46 -03:00
Lucas Rayan f62e7fd0a1 chore(release): v1.0.0
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
Primeira versão estável: valida ponta a ponta contra hardware real (Evil
Twin PSK e Enterprise, WPS, quebra com regras/máscara, relatório) e fecha
o roadmap de 0.1.0 quase inteiro. Changelog completo em CHANGELOG.md.

- Cargo.toml (workspace + app desktop) e tauri.conf.json: 0.1.0 → 1.0.0.
- README/ROADMAP (EN+PT-BR): refletem o que de fato existe agora
  (regras/máscara, .hc22000, WPS, Evil Twin, relatório), não mais "Next".
- docs/cli.md e docs/interface.md (EN+PT-BR): documentam --rules no
  crack e a aba Ataques/relatório da interface.
2026-10-24 15:00:57 -03:00
Lucas Rayan 62ce8b60d5 fix: avisos de clippy em crack_reader/crack_mask (bound duplicado em where) 2026-10-24 14:56:06 -03:00
Lucas Rayan d3ddf86317 chore: atualiza Cargo.lock (flate2, novas dependências transitivas) 2026-10-24 14:54:53 -03:00
Lucas Rayan 6e0cd7cf10 feat(ui): ataques (Evil Twin, regras/máscara, WPS), captura mais robusta
- app.js/app.css/index.html: telas novas para Evil Twin (PSK e
  Enterprise), regras de mutação/máscara na quebra de senha, WPS,
  indicador de modo (monitor/gerenciado) movido para baixo do seletor de
  interface, e aviso específico quando a placa não entrega captura de
  verdade (ver bug de ScanSnapshot.warning no engine).
- Lista de redes/BSSIDs não reordena mais a cada poll: ordem de chegada
  como critério de desempate e sinal arredondado em buckets de 4 dBm,
  porque reordenar pelo sinal instantâneo fazia as linhas flutuarem de
  posição debaixo do cursor a cada atualização.
- "Forçar handshake" religa a captura sozinho se ela tiver sido parada
  (esperando o AP reaparecer no canal certo antes de disparar o job, e
  travando o alvo numa variável local pra não perder o BSSID se o diálogo
  fechar no meio da espera).
- Grid da aba Ataques trocado de CSS grid pra colunas (alvenaria): cards
  com alturas muito diferentes na mesma linha deixavam buracos vazios
  enormes com align-items:start.
- check-ui-i18n.mjs: ajustes de cobertura para as novas telas.
2026-10-24 14:54:49 -03:00
Lucas Rayan a3ca73e610 feat: orquestra as novas funções no engine, CLI e app desktop; resiliência de rede
- engine.rs: expõe Evil Twin (PSK/Enterprise), crack com regras/máscara,
  relatório e exportação .hc22000 pros três front-ends (CLI, servidor web
  da UI, Tauri). set_monitor() agora solta a interface do NetworkManager
  antes de ativar modo monitor e devolve ao desativar — sem isso o NM
  reafirma a própria conexão por cima e a captura cai sozinha pouco
  depois ("está em modo managed"). Evil Twin grava um arquivo de estado
  em disco antes de soltar as interfaces; se o processo morrer no meio
  (Ctrl+C sem handler, kill -9, queda de energia), o próximo início do
  engine desfaz tudo sozinho (self_heal_eviltwin_leftovers) — sem isso as
  duas interfaces ficavam presas em managed=no pro NetworkManager pra
  sempre e o Wi-Fi inteiro sumia do painel do sistema. hostapd/hostapd-wpe
  que morrem na hora de subir agora reportam a saída real capturada em
  log, em vez de um processo "<defunct>" mudo. Novo
  ScanSnapshot.warning: avisa quando a sessão roda >12s sem receber
  nenhum quadro — sintoma de placa que aceita ativar modo monitor sem
  entregar captura de verdade (comum em Wi-Fi interno de notebook).
- sabia-cli (main.rs/web/mod.rs) e o app desktop (Tauri main.rs) ganham
  as rotas/comandos equivalentes pra tudo isso.
2026-10-24 14:54:35 -03:00
Lucas Rayan 7b39c67830 feat: relatório de auditoria redesenhado (capa, sumário, cabeçalho paginado)
- report.rs: capa em página A4 cheia com o logo do Sabiá embutido (base64,
  funciona offline), sumário com âncoras pra cada seção, cabeçalho/rodapé
  corridos via @page margin boxes (seção atual, numeração X/Y, selo "uso
  autorizado"), cards de KPI e achados com cor por severidade, tabelas
  com cabeçalho/zebra consistentes — paginação em A4 continua feita pelo
  Paged.js no navegador (sem depender de Chromium no servidor).
- Vendoriza paged.polyfill.min.js (MIT) — antes era carregado de fora.
- tauri.conf.json: CSP libera frame-src pro preview do relatório embutido
  (iframe) no app desktop.
2026-10-24 14:54:21 -03:00
Lucas Rayan 91aacba939 feat: Evil Twin (PSK + Enterprise/802.1X) e achados de análise de canais
- eviltwin.rs: clona um AP já visto na sessão (SSID aberto) numa segunda
  interface, com portal cativo pedindo a senha de novo. A senha só é
  aceita se bater de verdade contra o handshake real já capturado
  (CrackTarget::verify) — não é um "aceita qualquer coisa", é um oráculo
  criptográfico. Exige handshake quebrável já capturado desse BSSID e
  autorização explícita, mesma trilha de auditoria dos outros ataques.
- eviltwin_enterprise.rs: variante pra redes WPA2/WPA3-Enterprise
  (802.1X) — nelas a credencial viaja no próprio handshake EAP
  (PEAP/TTLS-MSCHAPv2), então o cliente autentica sozinho em qualquer AP
  que anuncie o SSID, sem portal nem ação humana. Orquestra hostapd-wpe
  (não vendorizado — é um patch no build do hostapd); gera certificado
  TLS autoassinado e eap_user genérico; parser best-effort do log de
  credenciais capturadas. Limitação documentada: cliente que valida a
  cadeia de certificado recusa a conexão.
- analysis.rs: achados de segurança (rede aberta, sem PMF, WPS ativo,
  possível evil twin real por nome duplicado) e recomendação de canal por
  banda com pontuação de interferência/sobreposição.
2026-10-24 14:54:06 -03:00
Lucas Rayan f83654506e feat: dicionário completo, regras de mutação, máscara, .hc22000 e WPS (reaver)
- crack.rs: dicionário padrão trocado pra wpa-full.txt.gz (rockyou +
  Probable-Wordlists top204k mesclados/deduplicados, 9,7M senhas,
  comprimido e embutido via LazyLock), PBKDF2 ~640x mais rápido
  (paralelização por núcleo + HMAC reaproveitado em vez de reconstruído a
  cada iteração), força bruta por máscara (rules.rs::Mask, sintaxe
  ?d?l?u?s?a) e regras de mutação tipo hashcat (maiúscula, sufixos
  numéricos/símbolos, leet simples).
- hashcat.rs: exportação de handshake/PMKID em .hc22000 (individual ou em
  lote), pro hashcat de verdade em vez de só o dicionário embutido.
- wps.rs: ataque WPS PIN via reaver (binário real do
  reaver-wps-fork-t6x, GPL-2, extraído em tempo de execução) e bully
  quando instalado no sistema.
- assets/wordlists/LICENSE.wpa-full documenta a atribuição (CC BY-SA 4.0
  do Probable-Wordlists; rockyou.txt como dado de vazamento de uso livre
  consolidado em pesquisa de segurança, mesmo padrão do Kali/SecLists).
2026-10-24 14:53:54 -03:00
Lucas Rayan 5477b8fb03 fix: captura de handshake real (endianness EAPOL) e enriquece o parsing 802.11
- eapol.rs: key_info/replay_counter/key_data_len liam little-endian; são
  big-endian (802.1X segue ordem de rede), então handshakes reais nunca
  fechavam mesmo com deauth funcionando. testutil.rs e os geradores de
  quadro de teste corrigidos junto, mais um teste contra bytes de wire
  reais (não só contra o próprio encoder).
- inject.rs: deauth/disassoc agora grava Sequence Control crescente por
  quadro — reenviar o mesmo quadro era descartado como duplicata pelo
  driver/AP depois do primeiro envio.
- dot11.rs/scan.rs: parsing de AP/cliente bem mais completo (PMF
  obrigatório, WPS detalhado, fabricante por OUI, país, largura de banda,
  padrão PHY, privacidade) e histórico de sinal/eventos por cliente.
- handshake.rs/ccmp.rs: deriva a Temporal Key (CCMP) quando a senha é
  quebrada, o suficiente pra decifrar o tráfego em claro do par
  (BSSID, estação) cujo handshake foi capturado.
2026-10-24 14:53:32 -03:00
160 changed files with 16199 additions and 910 deletions

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
name: Report a bug
description: Something did not work as expected
labels: [bug]
body:
- type: textarea
id: what-happened
attributes: { label: "What happened?", description: "Steps to reproduce, expected behavior, and actual behavior." }
validations: { required: true }
- type: input
id: system
attributes: { label: "Operating system and Sabiá version", placeholder: "Ubuntu 24.04 / sabia 1.0.3" }
- type: input
id: adapter
attributes: { label: "Wi-Fi adapter and mode", placeholder: "wlan0 (Intel AX200), monitor mode" }
- type: textarea
id: log
attributes: { label: "Terminal output (sabia scan / ui)", render: shell }
+8
View File
@@ -0,0 +1,8 @@
## What changes and why
## Checklist
- [ ] `cargo fmt --all` and `cargo clippy --workspace --all-targets` pass without warnings
- [ ] `cargo test --workspace` passes
- [ ] Nothing in this PR transmits captured data outside the local machine
- [ ] Commits follow Conventional Commits in English and code/comments are in English
- [ ] CHANGELOG updated (if there is a user-visible change)
+88
View File
@@ -0,0 +1,88 @@
name: CI
on:
# Documentation-only or local agent state changes do not trigger CI jobs.
push:
branches: [main]
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
pull_request:
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
concurrency:
group: ci-${{ gitea.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
# The runner persists /opt/hostedtoolcache across jobs: rustup toolchains,
# cargo registry, npm/apt caches, and target directories live there so nothing
# is re-downloaded or rebuilt from scratch on every run. Each job uses its own
# target directory so differing RUSTFLAGS do not invalidate another job's cache.
CARGO_HOME: /opt/hostedtoolcache/cargo
RUSTUP_HOME: /opt/hostedtoolcache/rustup
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
jobs:
lint:
name: Format and clippy
runs-on: ubuntu-latest
env:
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-lint
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: actions/setup-node@v7
with:
node-version: 22
- name: UI translation coverage
run: node scripts/check-ui-i18n.mjs
- run: cargo fmt --all -- --check
- run: cargo clippy --workspace --all-targets
# Linux-only runner (ARM64).
test:
name: Tests (Linux)
runs-on: ubuntu-latest
env:
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-test
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- run: cargo test --workspace
audit:
name: Dependency audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
# rustsec/audit-check depends on GitHub's checks API. cargo-audit itself
# needs nothing but the advisory database, and installing it once into the
# persistent CARGO_HOME makes every later run skip compilation.
- name: cargo audit
run: |
command -v cargo-audit >/dev/null || cargo install cargo-audit --locked
cargo audit
gui:
name: Check desktop app (Linux)
runs-on: ubuntu-latest
env:
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-gui
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Tauri system dependencies
timeout-minutes: 25
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Generate icons
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
env:
RUSTFLAGS: ""
+132
View File
@@ -0,0 +1,132 @@
name: Release
# Triggered when a v* tag is pushed: creates the Gitea Release entry with notes
# from CHANGELOG.md and uploads the compiled CLI (scripts/package-cli.sh) with
# a .sha256 checksum alongside each archive, plus the desktop bundles (Linux
# aarch64, Linux x86_64, and Windows via cargo-xwin).
#
# Updater signing (optional): SABIA_TAURI_SIGNING_PRIVATE_KEY and
# SABIA_TAURI_SIGNING_PRIVATE_KEY_PASSWORD, with unprefixed names as fallback.
# Without a key, the build succeeds without generating .sig files.
on:
push:
tags: ['v*']
env:
CARGO_TERM_COLOR: always
# Rust toolchains, cargo registry, and caches live on the persistent runner volume.
CARGO_HOME: /opt/hostedtoolcache/cargo
RUSTUP_HOME: /opt/hostedtoolcache/rustup
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
XWIN_CACHE_DIR: /opt/hostedtoolcache/xwin-cache
jobs:
release:
name: Create release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Release
env:
TAG: ${{ gitea.ref_name }}
TOKEN: ${{ gitea.token }}
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
run: |
# Re-running the workflow does not recreate an existing release.
if curl -fsS -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" >/dev/null 2>&1; then
echo "release $TAG already exists"; exit 0
fi
scripts/release-notes.sh "$TAG" > notes.md
pre=false; case "$TAG" in *-*) pre=true ;; esac
jq -n --arg tag "$TAG" --arg name "Sabiá ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
-H 'Content-Type: application/json' -d @release.json >/dev/null
cli:
name: CLI (${{ matrix.platform }})
needs: release
runs-on: ${{ matrix.runner }}
env:
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-cli-${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
include:
- { platform: linux-aarch64, target: aarch64-unknown-linux-gnu, apt: "", runner: linux-arm64 }
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu", runner: linux-amd64 }
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Platform cross-linker
if: ${{ matrix.apt != '' && matrix.runner != 'linux-amd64' }}
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/cli-x86_64 ${{ matrix.apt }}
- name: Build and package
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
- name: Upload to release
env:
TOKEN: ${{ gitea.token }}
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
run: scripts/release-upload.sh "${{ gitea.ref_name }}" $(cat packages.txt)
gui:
name: Desktop app (${{ matrix.platform }})
needs: release
runs-on: ${{ matrix.runner }}
env:
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-gui-${{ matrix.platform }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.SABIA_TAURI_SIGNING_PRIVATE_KEY || secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.SABIA_TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
strategy:
fail-fast: false
matrix:
include:
- { platform: linux-aarch64, runner: linux-arm64 }
- { platform: linux-x86_64, runner: linux-amd64 }
- { platform: windows, runner: linux-amd64 }
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Tauri dependencies (Linux aarch64)
if: matrix.platform == 'linux-aarch64'
timeout-minutes: 25
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Tauri dependencies (Linux x86_64)
if: matrix.platform == 'linux-x86_64'
timeout-minutes: 25
run: |
if [ "$(uname -m)" = aarch64 ]; then
dpkg --add-architecture amd64
. /etc/os-release
if [ "${ID:-}" = ubuntu ]; then
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
grep -q '^Architectures:' /etc/apt/sources.list.d/ubuntu.sources || sed -i '/^Types: deb/a Architectures: arm64' /etc/apt/sources.list.d/ubuntu.sources
printf 'Types: deb\nURIs: http://archive.ubuntu.com/ubuntu/\nSuites: %s %s-updates %s-security\nComponents: main universe\nArchitectures: amd64\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.sources
elif [ -f /etc/apt/sources.list ]; then
sed -i 's/^deb http/deb [arch=arm64] http/' /etc/apt/sources.list
printf 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-updates main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-security main universe\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.list
fi
fi
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-amd64 gcc-x86-64-linux-gnu g++-x86-64-linux-gnu pkg-config patchelf file squashfs-tools qemu-user libglib2.0-bin libwebkit2gtk-4.1-dev:amd64 libayatana-appindicator3-dev:amd64 librsvg2-dev:amd64
else
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
fi
- name: Cross-compilation tools (Windows)
if: matrix.platform == 'windows'
run: |
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-win nsis lld llvm clang
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
- name: Build and package
run: scripts/build-gui.sh "${{ matrix.platform }}"
- name: Upload to release
env:
TOKEN: ${{ gitea.token }}
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
run: scripts/release-upload.sh "${{ gitea.ref_name }}" dist-gui/*
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Enforces Conventional Commits 1.0.0 and English-only commit messages.
set -euo pipefail
msg_file="${1:?usage: commit-msg <file>}"
python3 - "$msg_file" <<'PY'
import re, sys
from pathlib import Path
raw = Path(sys.argv[1]).read_text(encoding="utf-8")
lines = [l for l in raw.splitlines() if not l.startswith("#")]
while lines and not lines[0].strip():
lines.pop(0)
if not lines:
sys.exit("commit-msg: empty commit message")
subject = lines[0].strip()
if subject.startswith(("Merge ", "Revert \"")):
sys.exit(0)
conv_re = re.compile(
r"^(feat|fix|chore|ci|docs|refactor|test|perf|build|style|revert)(\([a-z0-9_./-]+\))?!?: .+$"
)
if not conv_re.match(subject):
sys.exit(
f"commit-msg: invalid Conventional Commit header: {subject!r}\n"
"Expected: <type>[optional scope]: <description in English>\n"
"Allowed types: feat, fix, chore, ci, docs, refactor, test, perf, build, style, revert"
)
body = "\n".join(lines)
cleaned = re.sub(r"\b(Guará|Sabiá|Acauã|Ciberfauna)\b", "", body)
pt_accents = re.search(r"[áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ]", cleaned)
pt_words = re.search(
r"\b(não|para|como|quando|então|pelos|pelas|pelo|pela|estão|está|também|versão|"
r"corrige|adiciona|atualiza|remove|padroniza|implementa|ajusta|arquivo|arquivos)\b",
cleaned,
re.I,
)
if pt_accents or pt_words:
sys.exit("commit-msg: commit messages must be written 100% in English.")
PY
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -euo pipefail
exec "$(git rev-parse --show-toplevel)/scripts/check.sh"
-17
View File
@@ -1,17 +0,0 @@
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 }
-7
View File
@@ -1,7 +0,0 @@
## 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)
-83
View File
@@ -1,83 +0,0 @@
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 }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
jobs:
lint:
name: Formatação e clippy
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
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
test:
name: Testes (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test --workspace
audit:
name: Auditoria de dependências
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
gui:
name: Compila o app desktop (Linux)
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: apps/gui/src-tauri
- 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
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
env:
RUSTFLAGS: ""
-151
View File
@@ -1,151 +0,0 @@
name: Release
# Publica uma release pública ao empurrar uma tag vX.Y.Z (ex.: git tag v0.1.0 && git push --tags)
on:
push:
tags: ["v*.*.*"]
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
jobs:
create-release:
name: Cria a release
runs-on: ubuntu-24.04
outputs:
release_id: ${{ steps.rel.outputs.result }}
steps:
- uses: actions/checkout@v7
- name: Notas da versão (do CHANGELOG)
id: notes
run: |
v="${GITHUB_REF_NAME#v}"
v="${v%%-*}"
awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md
[ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md
- uses: actions/github-script@v9
id: rel
with:
script: |
const fs = require('fs');
const tag = context.ref.replace('refs/tags/', '');
const r = await github.rest.repos.createRelease({
owner: context.repo.owner, repo: context.repo.repo, tag_name: tag,
name: `Sabiá ${tag}`, body: fs.readFileSync('notes.md', 'utf8'),
draft: true, prerelease: tag.includes('-'),
});
return r.data.id;
cli:
name: CLI (${{ matrix.name }})
needs: create-release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { name: linux-x86_64, os: ubuntu-24.04, target: x86_64-unknown-linux-gnu }
- { name: linux-aarch64, os: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
- { name: macos-aarch64, os: macos-latest, target: aarch64-apple-darwin }
- { name: macos-x86_64, os: macos-latest, target: x86_64-apple-darwin }
- { name: windows-x86_64, os: windows-latest, target: x86_64-pc-windows-msvc }
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
- run: cargo build --release --locked -p sabia-cli --target ${{ matrix.target }}
- name: Empacota
shell: bash
run: |
name="sabia-${GITHUB_REF_NAME}-${{ matrix.name }}"
mkdir "$name"
if [ "$RUNNER_OS" = "Windows" ]; then
cp target/${{ matrix.target }}/release/sabia.exe "$name/"
else
cp target/${{ matrix.target }}/release/sabia "$name/"
fi
cp README.md LICENSE-MIT LICENSE-APACHE "$name/"
if [ "$RUNNER_OS" = "Windows" ]; then
7z a "$name.zip" "$name" >/dev/null; asset="$name.zip"
else
tar czf "$name.tar.gz" "$name"; asset="$name.tar.gz"
fi
if command -v sha256sum >/dev/null; then sha256sum "$asset" > "$asset.sha256"; else shasum -a 256 "$asset" > "$asset.sha256"; fi
echo "ASSET=$asset" >> "$GITHUB_ENV"
- name: Envia para a release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "$GITHUB_REF_NAME" "$ASSET" "$ASSET.sha256" --clobber
gui:
name: App desktop (${{ matrix.name }})
needs: create-release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { name: linux-x86_64, os: ubuntu-24.04, args: "" }
- { name: macos-aarch64, os: macos-latest, args: "--target aarch64-apple-darwin" }
- { name: macos-x86_64, os: macos-latest, args: "--target x86_64-apple-darwin" }
- { name: windows-x86_64, os: windows-latest, args: "" }
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ contains(matrix.args, 'apple') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: apps/gui/src-tauri
- 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'
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
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
projectPath: apps/gui/src-tauri
releaseId: ${{ needs.create-release.outputs.release_id }}
args: ${{ matrix.args }}
publish:
name: Publica a release
needs: [create-release, cli, gui]
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Notas da versão (do CHANGELOG)
run: |
v="${GITHUB_REF_NAME#v}"
v="${v%%-*}"
awk -v v="$v" '$0 ~ "^## \\[" v "\\]" {f=1; next} /^## \[/ {f=0} f' CHANGELOG.md > notes.md
[ -s notes.md ] || echo "Veja o CHANGELOG." > notes.md
- uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
await github.rest.repos.updateRelease({
owner: context.repo.owner, repo: context.repo.repo,
release_id: ${{ needs.create-release.outputs.release_id }},
name: `Sabiá ${context.ref.replace('refs/tags/', '')}`,
body: fs.readFileSync('notes.md', 'utf8'),
draft: false,
});
+27 -5
View File
@@ -1,13 +1,21 @@
/target
# Build artifacts
target/
apps/gui/src-tauri/target/
node_modules/
*.img
*.guara-session.json
.DS_Store
**/*.rs.bk
# Sabiá local captures and audit log
*.pcap
*.cap
.sabia-audit.log
node_modules/
apps/gui/src-tauri/target
# Configuração e memória local de agentes de IA — não faz parte do
# produto, fica só na máquina de desenvolvimento.
# Logo copied to the UI directory by build.rs
/apps/gui/ui/logo.png
# Local AI agent configuration and state — not part of the product
.claude/
.wolf/
.opencode/
@@ -18,3 +26,17 @@ apps/gui/src-tauri/target
CLAUDE.md
AGENTS.md
GEMINI.md
# Local environment files
.env
.env.*
# Local scratch files
*.eml
__pycache__/
*.pyc
# Packages generated by scripts/package-cli.sh and scripts/build-gui.sh
/guara-v*
/sabia-v*
/dist-gui
+105
View File
@@ -4,6 +4,111 @@
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
## [1.0.3] - 2026-10-30
### Fixed
- `deauth_directional` had no `#[cfg(target_os = "linux")]` of its own; every
non-Linux build failed with a duplicate-definition error and unresolved
Linux-only imports. `v1.0.1` and `v1.0.2` never got a working Windows
desktop app for this reason, and `v1.0.2`'s Linux package was mislabeled
`1.0.1` (the version in `Cargo.toml` was not bumped). Use `v1.0.3`.
## [1.0.2] - 2026-10-30
### Fixed
- The desktop app build on CI looked for its bundle under `target/`, but the
runner's Cargo target directory is elsewhere and persists between runs; the
Linux and Windows jobs failed before publishing anything. `v1.0.1`'s desktop
app was never published for this reason — use `v1.0.2`.
## [1.0.1] - 2026-10-30
The project moved from GitHub to Gitea, and the update check followed it.
### Changed
- **Updates now come from Gitea.** `sabia update` and the desktop app's "Update now"
read the latest release from `git.lucasrguerra.dev.br` instead of the GitHub API.
Versions up to 1.0.0 still look at GitHub, which is no longer reachable, so they
cannot find this release: download 1.0.1 once, and later updates are automatic.
- Release packages: the CLI for Linux x86_64 and aarch64 (built by the Gitea CI when
a `v*` tag is pushed, with a `.sha256` next to each package), and the desktop app
for Linux (`.AppImage`, `.deb`) and Windows (NSIS `.exe` installer, cross-compiled
from Linux by `scripts/build-gui.sh`). **There is no Windows CLI and no macOS build
any more**: the CLI is Linux-only, and macOS needs Apple's SDK and code signing,
which this project's CI cannot provide.
### Fixed
- `sabia update` refuses to run from the desktop app's executable (`sabia-gui`).
The release package contains the CLI, and replacing the app with it would have
broken the app; the app now points to the downloads page instead.
## [1.0.0] - 2026-10-24
First stable release: real-hardware validation across the whole attack surface, a much more complete cracking pipeline, two new attacks (WPS, Evil Twin), a redesigned report, and several reliability fixes found only by testing against live Wi-Fi networks.
### Added
- **Cracking:** default dictionary replaced by a 9.7M-password list (rockyou +
Probable-Wordlists, deduplicated, gzip-embedded); hashcat-style **mutation
rules** (uppercase, numeric/symbol suffixes, simple leet); **mask brute
force** (`?d?l?u?s?a` syntax); **`.hc22000` export** (single or batch) to
hand a handshake to hashcat/John.
- **WPS attack:** PIN/Pixie Dust via a bundled `reaver` (extracted at
runtime), or `bully` when installed on the system.
- **Evil Twin:** clones an already-seen AP (open SSID) on a second interface
with a captive portal, for WPA/WPA2-PSK — the submitted password is only
accepted after being verified against the real captured handshake, never
"accepts anything". An **Enterprise (802.1X/EAP)** variant captures
credentials automatically via `hostapd-wpe`, no portal or human action
needed, since the credential travels in the EAP handshake itself.
- **CCMP traffic decryption** for a client once its handshake's password is
known (Temporal Key derived from the cracked PMK).
- **Channel-interference scoring** per band and **security findings** (open
network, missing PMF, WPS exposed, possible evil twin by duplicate SSID).
- **Redesigned audit report:** full-page cover with embedded logo, table of
contents, running header/footer via `@page` margin boxes (current
section, page numbers), KPI and findings cards with severity colors —
still paginated by Paged.js in the browser, printable to PDF.
- `ScanSnapshot.warning`: tells you when a capture has been running for a
while without receiving a single frame — a card that accepts turning
monitor mode on without actually delivering capture (common with
built-in laptop Wi-Fi) now says so, instead of silently "listening".
### Fixed
- **Handshake capture never completed on real networks:** `eapol.rs` read
`key_info`/`replay_counter`/`key_data_len` as little-endian; those
802.1X fields are big-endian. Deauth worked, EAPOL frames were seen, but
`HandshakeTracker` silently discarded every one — 0 handshakes no matter
how much traffic was generated. The project's own synthetic test frames
made the same mistake symmetrically, so round-trip tests never caught
it; verified live against a real AP after the fix.
- **Monitor mode fighting NetworkManager:** `set_monitor()` now releases
the interface from NetworkManager before switching modes and returns it
afterward — without this, NM reasserted its own connection within
seconds and capture died with "is in managed mode".
- **Evil Twin left the system half-configured if the process died mid-run**
(Ctrl+C, `kill -9`, power loss): both interfaces stuck `managed=no`
forever, Wi-Fi gone from the system panel. A state file plus a
self-healing pass on every engine start now undoes this automatically;
`sabia ui` also handles SIGINT gracefully.
- **Repeated deauth/disassoc frames were dropped as duplicates** after the
first send — the same Sequence Control was reused on every injection;
now increments per frame.
- **Network list reordered under the cursor** during capture, as signal-based
sorting reacted to every 1-2 dBm fluctuation; order is now stable
(arrival order, signal rounded to 4 dBm buckets).
- **"Force handshake" failed if capture had been stopped:** it now resumes
capture automatically and waits for the target to reappear with a known
channel before starting the job, instead of locking onto whatever
channel the scanner happened to be hopping through.
- **Attacks tab layout left large empty gaps** when two cards of very
different heights shared a grid row; switched to a masonry-style column
layout.
### Changed
- `crack_reader`/`crack_mask` parallelize across available cores and reuse
the HMAC base per iteration instead of reconstructing it — roughly 640x
faster in release builds versus the original debug/single-thread path.
## [0.1.0] - 2026-10-23
First release: CLI, desktop app and shared core.
+111
View File
@@ -4,6 +4,117 @@
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
## [1.0.3] - 2026-10-30
### Corrigido
- O `deauth_directional` não tinha o próprio `#[cfg(target_os = "linux")]`;
todo build fora do Linux falhava com uma definição duplicada e imports só de
Linux fora de escopo. Por isso a `v1.0.1` e a `v1.0.2` nunca tiveram um app
desktop funcional para Windows, e o pacote Linux da `v1.0.2` saiu rotulado
`1.0.1` (a versão no `Cargo.toml` não tinha sido atualizada). Use a `v1.0.3`.
## [1.0.2] - 2026-10-30
### Corrigido
- O build do app desktop no CI procurava o pacote em `target/`, mas o
diretório do Cargo no runner é outro e persiste entre execuções; os jobs de
Linux e Windows falhavam antes de publicar qualquer coisa. Por isso o app
desktop da `v1.0.1` nunca foi publicado — use a `v1.0.2`.
## [1.0.1] - 2026-10-30
O projeto saiu do GitHub para o Gitea, e a verificação de atualização foi junto.
### Alterado
- **As atualizações agora vêm do Gitea.** O `sabia update` e o "Atualizar agora" do app
desktop leem a última release em `git.lucasrguerra.dev.br`, e não mais a API do
GitHub. As versões até a 1.0.0 ainda olham o GitHub, que não responde mais, e por isso
não enxergam esta release: baixe a 1.0.1 uma vez, e as próximas atualizações são automáticas.
- Pacotes da release: o CLI para Linux x86_64 e aarch64 (compilado pelo CI do Gitea
quando uma tag `v*` é enviada, com um `.sha256` ao lado de cada pacote) e o app desktop
para Linux (`.AppImage`, `.deb`) e Windows (instalador NSIS `.exe`, compilado a partir
do Linux pelo `scripts/build-gui.sh`). **Não há mais CLI para Windows nem build para
macOS**: o CLI é só para Linux, e o macOS exige o SDK e a assinatura da Apple, que o CI
deste projeto não tem.
### Corrigido
- O `sabia update` se recusa a rodar a partir do executável do app desktop (`sabia-gui`).
O pacote da release contém o CLI, e trocar o app por ele o quebraria; o app passa a
apontar para a página de downloads.
## [1.0.0] - 2026-10-24
Primeira versão estável: validação em hardware real por toda a superfície de ataque, um pipeline de quebra bem mais completo, dois ataques novos (WPS, Evil Twin), relatório redesenhado e várias correções de confiabilidade que só apareceram testando contra redes Wi-Fi de verdade.
### Adicionado
- **Quebra de senha:** dicionário padrão trocado por uma lista de 9,7 milhões
de senhas (rockyou + Probable-Wordlists, deduplicados, comprimida e
embutida no binário); **regras de mutação** estilo hashcat (maiúscula,
sufixos numéricos/símbolos, leet simples); **força bruta por máscara**
(sintaxe `?d?l?u?s?a`); **exportação `.hc22000`** (individual ou em lote)
para levar o handshake direto ao hashcat/John.
- **Ataque WPS:** PIN/Pixie Dust via `reaver` embutido (extraído em tempo de
execução), ou `bully` quando instalado no sistema.
- **Evil Twin:** clona um AP já visto na sessão (SSID aberto) numa segunda
interface com portal cativo, para WPA/WPA2-PSK — a senha só é aceita
depois de conferida de verdade contra o handshake real já capturado,
nunca "aceita qualquer coisa". Uma variante **Enterprise (802.1X/EAP)**
captura credenciais automaticamente via `hostapd-wpe`, sem portal nem
ação humana, já que a credencial viaja no próprio handshake EAP.
- **Decriptação de tráfego CCMP** de um cliente assim que a senha do
handshake dele é conhecida (Temporal Key derivada da PMK quebrada).
- **Pontuação de interferência de canais** por banda e **achados de
segurança** (rede aberta, PMF ausente, WPS exposto, possível evil twin
por SSID duplicado).
- **Relatório de auditoria redesenhado:** capa em página cheia com o logo
embutido, sumário com âncoras, cabeçalho/rodapé corridos via margin
boxes do `@page` (seção atual, numeração de página), cards de KPI e
achados com cor por severidade — a paginação continua feita pelo
Paged.js no navegador, imprimível em PDF.
- `ScanSnapshot.warning`: avisa quando uma captura está rodando há um
tempo sem receber nenhum quadro — uma placa que aceita ativar modo
monitor sem realmente entregar captura (comum em Wi-Fi interno de
notebook) agora diz isso, em vez de ficar "escutando o ar" caladinha.
### Corrigido
- **Captura de handshake nunca completava em rede real:** `eapol.rs` lia
`key_info`/`replay_counter`/`key_data_len` como little-endian; esses
campos do 802.1X são big-endian. O deauth funcionava, os quadros EAPOL
eram vistos, mas o `HandshakeTracker` descartava todos silenciosamente —
0 handshakes não importa quanto tráfego fosse gerado. Os próprios
quadros sintéticos de teste do projeto cometiam o mesmo engano
simetricamente, então os testes de ida e volta nunca pegaram isso;
verificado ao vivo contra um AP real depois da correção.
- **Modo monitor brigando com o NetworkManager:** `set_monitor()` agora
solta a interface do NetworkManager antes de trocar de modo e devolve
depois — sem isso o NM reafirmava a própria conexão em segundos e a
captura caía com "está em modo managed".
- **Evil Twin deixava o sistema pela metade se o processo morresse no
meio** (Ctrl+C, `kill -9`, queda de energia): as duas interfaces ficavam
presas em `managed=no` pra sempre, Wi-Fi sumia do painel do sistema. Um
arquivo de estado mais uma rotina de autocura a cada início do engine
desfazem isso sozinhos agora; o `sabia ui` também trata SIGINT
graciosamente.
- **Quadros repetidos de deauth/disassoc eram descartados como duplicata**
depois do primeiro envio — o mesmo Sequence Control era reusado em toda
injeção; agora incrementa por quadro.
- **Lista de redes reordenava debaixo do cursor** durante a captura, porque
a ordenação por sinal reagia a cada oscilação de 1-2 dBm; a ordem agora
é estável (ordem de chegada, sinal arredondado em buckets de 4 dBm).
- **"Forçar handshake" falhava se a captura tivesse sido parada:** agora
religa a captura sozinho e espera o alvo reaparecer com canal conhecido
antes de disparar o job, em vez de travar no canal em que o scanner por
acaso estava saltando.
- **Grid da aba Ataques deixava buracos vazios grandes** quando dois cards
de alturas muito diferentes caíam na mesma linha; trocado por um layout
de colunas tipo alvenaria.
### Alterado
- `crack_reader`/`crack_mask` paralelizam entre os núcleos disponíveis e
reaproveitam a base do HMAC por iteração em vez de reconstruí-la — cerca
de 640x mais rápido em build release comparado ao caminho original
debug/single-thread.
## [0.1.0] - 2026-10-23
Primeira versão: CLI, app desktop e núcleo compartilhado.
+18 -6
View File
@@ -7,19 +7,27 @@ 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.
4. **100% English codebase and comments.** All source code, comments, scripts, CI workflows, and commit messages must be written in English, while the GUI/CLI remains bilingual (PT-BR and EN via `i18n-en.js`).
## 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
scripts/install-hooks.sh # once: enables commit-msg and pre-push Git hooks
scripts/check.sh # fast (seconds): fmt, clippy as in CI, i18n, scripts, workflows, semver
scripts/check.sh --full # adds cargo test --workspace and cargo audit
```
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.
CI runs on Linux; run the check before pushing: almost everything that fails CI shows up here in seconds. Documentation-only changes (`*.md`, `docs/`) skip 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.
Live capture exists only on Linux (`AF_PACKET` + netlink); the code behind Windows and macOS paths still compiles and is covered by `cargo check`.
## Releases and Versioning
Versions strictly follow **Semantic Versioning 2.0.0** (`MAJOR.MINOR.PATCH`), synchronized across `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml`, and `apps/gui/src-tauri/tauri.conf.json` (enforced by `scripts/check.sh`).
Pushing a `v*` tag makes the Gitea CI create the release (notes come from `CHANGELOG.md`, so update it first) and attach the Linux CLI (x86_64 and aarch64) and the desktop app for Linux (x86_64 and aarch64) and Windows.
There is no macOS build: it needs Apple's SDK and code signing, which a Linux runner cannot provide.
## Where to work
@@ -38,6 +46,10 @@ The source text of the interface is **Portuguese**; English comes from `apps/gui
Keep the pt/en pair: every `*.md` and `docs/*` file has a Portuguese version (`*.pt-BR.md`, `docs/pt-BR/`) and an English one.
## Commits
Commits must follow **Conventional Commits 1.0.0** in English (`feat:`, `fix:`, `chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`), e.g. `fix(eapol): validate replay counter on message 2`. One logical change per PR. Enforced automatically by `.githooks/commit-msg`.
## License
By contributing, you agree to license under [MIT](LICENSE-MIT) **or** [Apache-2.0](LICENSE-APACHE), like the rest of the project.
+18 -6
View File
@@ -7,19 +7,27 @@ 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.
4. **Código, comentários e commits 100% em inglês.** Todo código-fonte, comentários, scripts, workflows de CI e mensagens de commit devem ser escritos em inglês, enquanto a interface gráfica/CLI permanece bilíngue (PT-BR e EN via `i18n-en.js`).
## 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
scripts/install-hooks.sh # uma vez: ativa os hooks commit-msg e pre-push
scripts/check.sh # rápido (segundos): fmt, clippy como no CI, tradução, scripts, workflows, semver
scripts/check.sh --full # inclui cargo test --workspace e cargo audit
```
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.
O CI roda em Linux; rode a verificação antes do push: quase tudo que faz o CI falhar aparece aqui em segundos. Mudanças só de documentação (`*.md`, `docs/`) pulam o 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.
A captura ao vivo só existe no Linux (`AF_PACKET` + netlink); o código dos caminhos de Windows e macOS continua compilando e é coberto pelo `cargo check`.
## Releases e Versionamento
As versões seguem estritamente o **Versionamento Semântico 2.0.0** (`MAJOR.MINOR.PATCH`), sincronizadas entre `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml` e `apps/gui/src-tauri/tauri.conf.json` (validado por `scripts/check.sh`).
Enviar uma tag `v*` faz o CI do Gitea criar a release (as notas vêm do `CHANGELOG.md`, então atualize-o antes) e anexar o CLI para Linux (x86_64 e aarch64) e o app desktop para Linux (x86_64 e aarch64) e Windows.
Não há build para macOS: ele exige o SDK e a assinatura da Apple, que um runner Linux não tem.
## Onde mexer
@@ -38,6 +46,10 @@ O texto-fonte da interface é em **português**; o inglês vem de `apps/gui/ui/i
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.
## Commits
Os commits devem seguir **Conventional Commits 1.0.0** em inglês (`feat:`, `fix:`, `chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`), ex.: `fix(eapol): validate replay counter on message 2`. Uma mudança lógica por PR. Validado automaticamente por `.githooks/commit-msg`.
## Licença
Ao contribuir, você concorda em licenciar sob [MIT](LICENSE-MIT) **ou** [Apache-2.0](LICENSE-APACHE), como o resto do projeto.
Generated
+75 -2
View File
@@ -2,6 +2,23 @@
# 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 = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "anstream"
version = "1.0.0"
@@ -88,6 +105,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "clap"
version = "4.6.7"
@@ -143,6 +170,15 @@ 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 = "crypto-common"
version = "0.1.7"
@@ -209,6 +245,16 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "flate2"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -245,6 +291,15 @@ dependencies = [
"digest",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -285,6 +340,16 @@ version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[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 = "nix"
version = "0.31.3"
@@ -375,7 +440,7 @@ dependencies = [
[[package]]
name = "sabia-cli"
version = "0.1.0"
version = "1.0.4"
dependencies = [
"clap",
"ctrlc",
@@ -386,8 +451,10 @@ dependencies = [
[[package]]
name = "sabia-core"
version = "0.1.0"
version = "1.0.4"
dependencies = [
"aes",
"flate2",
"hmac",
"libc",
"md-5",
@@ -465,6 +532,12 @@ dependencies = [
"digest",
]
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]]
name = "strsim"
version = "0.11.1"
+6 -2
View File
@@ -3,10 +3,10 @@ resolver = "2"
members = ["crates/sabia-core", "crates/sabia-cli"]
[workspace.package]
version = "0.1.0"
version = "1.0.4"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/lucasrguerra/sabia"
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
authors = ["Sabiá contributors"]
rust-version = "1.80"
@@ -15,3 +15,7 @@ lto = "thin"
codegen-units = 1
opt-level = 3
strip = true
panic = "abort"
[profile.dev.package."*"]
opt-level = 3
+28 -21
View File
@@ -1,21 +1,25 @@
<p align="center"><img src="apps/gui/ui/sabia.png" width="300" alt="Sabiá"></p>
<p align="center"><img src="logo.png" width="300" alt="Sabiá"></p>
<h1 align="center">Sabiá</h1>
<p align="center">Free and open Wi-Fi auditing.<br>Terminal and desktop app · live capture on Linux, offline analysis on Windows, Linux and macOS · authorized use.</p>
<p align="center">Free and open Wi-Fi auditing.<br>Terminal and desktop app · live capture on Linux, offline analysis on Windows too · authorized use.</p>
<p align="center"><a href="docs/cli.md">Terminal</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/architecture.md">Architecture</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
<p align="center"><a href="docs/cli.md">Terminal</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/architecture.md">Architecture</a> · <a href="docs/support.md">Support</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
<p align="center"><b>English</b> · <a href="README.pt-BR.md">Português</a></p>
<p align="center"><img src="docs/img/networks-light-en.png" width="900" alt="The Sabiá desktop app capturing Wi-Fi frames in monitor mode, showing visible networks with signal bars, channels, security badges and associated clients"><br><sub>The real interface running with synthetic audit data (simulated networks, no real third-party data).</sub></p>
---
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.
- **Network discovery:** hops across the **2.4, 5 and 6 GHz** channels and lists access points and associated clients, with SSID, BSSID, channel, signal, security type, WPS details and vendor.
- **Monitor mode without scripts:** turns the card's monitor mode on and off straight through netlink/`iw`, recovering automatically if NetworkManager tries to reclaim the interface, 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; decrypts CCMP traffic for a client once its password is known.
- **Cracking, three ways:** dictionary (9.7M passwords bundled by default, rockyou + Probable-Wordlists), hashcat-style **mutation rules**, and **mask brute force** (`?d?l?u?s?a`) — recomputes the PMK with **PBKDF2-HMAC-SHA1** and checks the **MIC** (4-way) or the **PMKID** — for WPA/WPA2-PSK. Export to **`.hc22000`** to hand the handshake to hashcat/John instead.
- **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`).
- **Deauthentication and WPS:** directional/continuous _deauth_, PMF probing, clientless PMKID hunting, and a WPS PIN/Pixie Dust attack (bundled reaver, or bully when installed) — all gated behind an explicit authorization confirmation (`--yes` / checkbox).
- **Evil Twin:** clones an already-seen AP with a captive portal for WPA/WPA2-PSK — the submitted password is only accepted after it's verified against the real captured handshake, never "accepts anything" — plus an Enterprise (802.1X/EAP) variant that captures credentials automatically, no portal needed.
- **Audit report:** a paginated, printable report (cover, table of contents, findings, channel scoring, audit trail) generated locally.
- **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.
@@ -35,17 +39,18 @@ 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` |
| Windows | — | `.exe` installer |
| 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+).
The CLI is Linux-only; the desktop app ships for Linux and Windows. Each CLI
package has a `.sha256` next to it. Build from source: `cargo build --release
-p sabia-cli` (Rust 1.80+, Linux).
**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.
**Updates:** the desktop app checks Gitea on startup; when a new version exists, a button offers **Update now**. The CLI updates the same way (`sabia update`); 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.
> The binaries are **not signed yet**: Windows SmartScreen 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.
**Capture and monitor mode require Linux and root** — the only platform the CLI ships for (`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
@@ -65,6 +70,8 @@ Authorized deauthentication testing and all other commands (`audit`, `update`, `
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)**.
<p align="center"><img src="docs/img/networks-dark-en.png" width="640" alt="The networks table in the dark theme"> <img src="docs/img/attack-dark-en.png" width="640" alt="Network detail dialog showing attacks and authorization"></p>
- 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.
@@ -72,11 +79,11 @@ Desktop app (Tauri) with an interface sidebar, a capture panel with an animated
## 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.
- **Capture is Linux-only**, with a card that supports **monitor mode** (and injection, for `deauth`); the CLI is not built for other platforms. On Windows Sabiá's desktop app 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.
- **Evil Twin's self-signed certificate:** the Enterprise variant's TLS certificate isn't trusted by clients that validate the chain (common on updated corporate/MDM devices) — that's the correct defense working, not a bug.
- **Windows:** the code compiles and is tested in CI, but live capture does not run there; the desktop app does offline analysis only. There is no macOS build: Tauri needs the macOS SDK and code signing, both tied to Apple hardware and an Apple Developer account, which this project's CI (a single Linux runner) does not have.
## Where it's going
@@ -84,10 +91,10 @@ 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 |
| **Done** | Monitor mode via netlink (with NetworkManager recovery), 2.4/5/6 GHz scanning, 4-way and PMKID capture, CCMP decryption, dictionary/rules/mask cracking (WPA/WPA2-PSK), `.hc22000` export, `.pcap` read/write, authorized `deauth`/PMF/PMKID, WPS attack, Evil Twin (PSK + Enterprise), channel scoring and findings, paginated audit report, hash-chained audit trail, `doctor`, CLI + desktop app, one-click update |
| **Next** | GPU acceleration for cracking, capture on Windows (Npcap) |
| **Not planned** | A macOS build — see [Known limits](#known-limits) |
| **Later** | WPA3/SAE downgrade, validation on varied hardware, signed release binaries |
## Development
+28 -21
View File
@@ -1,21 +1,25 @@
<p align="center"><img src="apps/gui/ui/sabia.png" width="300" alt="Sabiá"></p>
<p align="center"><img src="logo.png" width="300" alt="Sabiá"></p>
<h1 align="center">Sabiá</h1>
<p align="center">Auditoria de Wi-Fi livre e gratuita.<br>Terminal e interface gráfica · captura no Linux, análise offline em Windows, Linux e macOS · uso autorizado.</p>
<p align="center">Auditoria de Wi-Fi livre e gratuita.<br>Terminal e interface gráfica · captura no Linux, análise offline também em Windows · uso autorizado.</p>
<p align="center"><a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
<p align="center"><a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="docs/pt-BR/suporte.md">Suporte</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
<p align="center"><a href="README.md">English</a> · <b>Português</b></p>
<p align="center"><img src="docs/img/networks-light-pt.png" width="900" alt="O aplicativo desktop do Sabiá capturando quadros Wi-Fi em modo monitor, mostrando redes visíveis com barras de sinal, canais, selos de segurança e clientes associados"><br><sub>A interface de verdade rodando com dados sintéticos de auditoria (redes simuladas, sem dados reais de terceiros).</sub></p>
---
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.
- **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, tipo de segurança, detalhes de WPS e fabricante.
- **Modo monitor sem scripts:** liga e desliga o modo monitor da placa direto pelo netlink/`iw`, com recuperação automática se o NetworkManager tentar retomar a interface, 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; decifra tráfego CCMP de um cliente assim que a senha dele é conhecida.
- **Quebra, de três jeitos:** dicionário (9,7 milhões de senhas por padrão, rockyou + Probable-Wordlists), **regras de mutação** estilo hashcat e **força bruta por máscara** (`?d?l?u?s?a`) — recalcula a PMK com **PBKDF2-HMAC-SHA1** e confere o **MIC** (4-way) ou o **PMKID** — para WPA/WPA2-PSK. Exporta em **`.hc22000`** para levar o handshake direto ao hashcat/John.
- **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`).
- **Desautenticação e WPS:** _deauth_ direcional/contínuo, teste de PMF, caça de PMKID sem cliente, e ataque WPS PIN/Pixie Dust (reaver embutido, ou bully quando instalado) — tudo atrás de confirmação explícita de autorização (`--yes`/checkbox).
- **Evil Twin:** clona um AP já visto com portal cativo para WPA/WPA2-PSK — a senha só é aceita depois de conferida contra o handshake real já capturado, nunca "aceita qualquer coisa" — mais uma variante Enterprise (802.1X/EAP) que captura credenciais automaticamente, sem portal.
- **Relatório de auditoria:** relatório paginado e imprimível (capa, sumário, achados, pontuação de canais, trilha de auditoria) gerado localmente.
- **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.
@@ -35,17 +39,18 @@ 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` |
| Windows | — | instalador `.exe` |
| 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+).
O CLI é só para Linux; o app desktop sai para Linux e Windows. Cada pacote do
CLI tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p
sabia-cli` (Rust 1.80+, Linux).
**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.
**Atualizações:** o app desktop consulta o Gitea ao abrir; havendo versão nova, um botão oferece **Atualizar agora**. O CLI atualiza do mesmo jeito (`sabia update`); 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.
> Os binários ainda **não são assinados**: o SmartScreen do Windows vai 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.
**Captura e modo monitor exigem Linux e privilégio de root** — a única plataforma para a qual o CLI é compilado (`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
@@ -65,6 +70,8 @@ Teste autorizado de desautenticação e todos os outros comandos (`audit`, `upda
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)**.
<p align="center"><img src="docs/img/networks-dark-pt.png" width="640" alt="A tabela de redes no tema escuro"> <img src="docs/img/attack-dark-pt.png" width="640" alt="Diálogo de detalhes da rede com opções de ataque e autorização"></p>
- 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.
@@ -72,11 +79,11 @@ App desktop (Tauri) com sidebar de interfaces, painel de captura com linha de si
## 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.
- **Captura só no Linux**, com placa que suporte **modo monitor** (e injeção, para `deauth`); o CLI não é compilado para outras plataformas. Em Windows o app desktop do 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.
- **Certificado autoassinado do Evil Twin Enterprise:** clientes que validam a cadeia de certificado da rede (comum em perfil corporativo/MDM atualizado) não confiam nele — é a defesa correta funcionando, não um bug.
- **Windows:** o código compila e é testado em CI, mas a captura ao vivo não roda lá; o app desktop faz só análise offline. Não há build para macOS: o Tauri precisa do SDK do macOS e de assinatura, os dois presos a hardware Apple e a uma conta Apple Developer, que o CI deste projeto (um único runner Linux) não tem.
## Para onde vai
@@ -84,10 +91,10 @@ 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 |
| **Feito** | Modo monitor por netlink (com recuperação do NetworkManager), varredura 2.4/5/6 GHz, captura de 4-way e PMKID, decriptação CCMP, quebra por dicionário/regras/máscara (WPA/WPA2-PSK), exportação `.hc22000`, escrita e leitura de `.pcap`, `deauth`/PMF/PMKID autorizados, ataque WPS, Evil Twin (PSK + Enterprise), pontuação de canais e achados, relatório de auditoria paginado, trilha de auditoria por hash, `doctor`, CLI + app desktop, atualização em um clique |
| **Próximo** | Aceleração por GPU na quebra, captura em Windows (Npcap) |
| **Fora de plano** | Um build para macOS — veja [Limites conhecidos](#limites-conhecidos) |
| **Depois** | WPA3/SAE _downgrade_, validação em hardware variado, binários de release assinados |
## Desenvolvimento
+18 -15
View File
@@ -2,34 +2,37 @@
**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.
Sabiá does Wi-Fi auditing: it discovers networks, captures handshakes (4-way and PMKID), cracks WPA/WPA2-PSK by dictionary/rules/mask, assesses WPS, PMF and evil twin exposure, and tests deauthentication and rogue-AP attacks with authorization. Always for authorized use, with a local audit trail.
## Done (0.1.0)
## Done (1.0.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.
- **Monitor mode** and channel control via netlink/`iw`, with automatic recovery from NetworkManager reclaiming the interface.
- **Scanning** of 2.4, 5 and 6 GHz: APs and clients, with SSID, BSSID, channel, signal, security, WPS details, vendor and PHY standard.
- 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.
- Capture of the **4-way handshake (EAPOL)** and the **PMKID**; **CCMP traffic decryption** once the password is known.
- **Offline cracking** (PBKDF2-HMAC-SHA1, MIC and PMKID) for WPA/WPA2-PSK: a 9.7M-password dictionary (rockyou + Probable-Wordlists) by default, hashcat-style **mutation rules** and **mask brute force**.
- **`.hc22000` export** to take the handshake straight to hashcat/John.
- **`.pcap`** read and write; offline `.pcap` analysis.
- Authorized **`deauth`** (`--yes` flag).
- Authorized **`deauth`** (`--yes` flag), directional and continuous jobs, PMF probing, clientless PMKID hunting.
- **WPS attack** (PIN/Pixie Dust) via a bundled reaver, or bully when installed.
- **Evil Twin** with captive portal for WPA/WPA2-PSK (password only accepted after cryptographic verification against the real handshake) and a credential-capture variant for WPA2/WPA3-Enterprise (802.1X/EAP).
- Channel-interference scoring and security findings (open network, no PMF, WPS exposed, possible evil twin).
- Redesigned, paginated **audit report** (cover, table of contents, running header/footer) that prints to PDF from the browser.
- 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
## Later
- **WPA3/SAE**: detection and downgrade paths where applicable.
- Validation on **varied hardware** (chipsets and regulatory domains).
- Signing of release binaries (SmartScreen/Gatekeeper).
## Long term
- GPU-accelerated cracking backend (hashcat integration beyond export).
- Packaged distribution (`apt`/`brew`/winget).
+18 -15
View File
@@ -2,34 +2,37 @@
[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.
O Sabiá faz auditoria de Wi-Fi: descobre redes, captura handshakes (4-way e PMKID), quebra WPA/WPA2-PSK por dicionário/regras/máscara, avalia exposição a WPS, PMF e evil twin, e testa desautenticação e ataques de rogue AP com autorização. Sempre de uso autorizado, com trilha de auditoria local.
## Feito (0.1.0)
## Feito (1.0.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.
- Controle de **modo monitor** e de canal por netlink/`iw`, com recuperação automática quando o NetworkManager tenta retomar a interface.
- **Varredura** de 2.4, 5 e 6 GHz: APs e clientes, com SSID, BSSID, canal, sinal, segurança, detalhes de WPS, fabricante e padrão PHY.
- 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.
- Captura de **4-way handshake (EAPOL)** e de **PMKID**; **decriptação de tráfego CCMP** quando a senha é conhecida.
- **Quebra offline** (PBKDF2-HMAC-SHA1, MIC e PMKID) para WPA/WPA2-PSK: dicionário padrão de 9,7 milhões de senhas (rockyou + Probable-Wordlists), **regras de mutação** estilo hashcat e **força bruta por máscara**.
- **Exportação `.hc22000`** para levar o handshake direto ao hashcat/John.
- Escrita e leitura de **`.pcap`**; análise de `.pcap` offline.
- **`deauth`** autorizado (flag `--yes`).
- **`deauth`** autorizado (flag `--yes`), jobs direcionais e contínuos, teste de PMF, caça de PMKID sem cliente.
- **Ataque WPS** (PIN/Pixie Dust) via reaver embutido, ou bully quando instalado.
- **Evil Twin** com portal cativo para WPA/WPA2-PSK (senha só aceita após verificação criptográfica contra o handshake real) e variante de captura automática de credencial para WPA2/WPA3-Enterprise (802.1X/EAP).
- Pontuação de interferência de canais e achados de segurança (rede aberta, sem PMF, WPS exposto, possível evil twin).
- **Relatório de auditoria** redesenhado e paginado (capa, sumário, cabeçalho/rodapé corridos) que imprime em PDF pelo navegador.
- **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
## Depois
- **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).
## Longo prazo
- Backend de quebra acelerado por GPU (integração com hashcat além da exportação).
- Distribuição empacotada (`apt`/`brew`/winget).
+1 -1
View File
@@ -2,6 +2,6 @@
**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á 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 private vulnerability reporting feature of the repository on Gitea. 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).
+1 -1
View File
@@ -2,6 +2,6 @@
[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á 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 pelo recurso de relato privado de vulnerabilidade do repositório no Gitea. 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).
+34 -2
View File
@@ -8,6 +8,17 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "1.1.5"
@@ -322,6 +333,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "combine"
version = "4.6.9"
@@ -1474,6 +1495,15 @@ dependencies = [
"cfb",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "ipnet"
version = "2.12.2"
@@ -2508,8 +2538,10 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "sabia-core"
version = "0.1.0"
version = "1.0.4"
dependencies = [
"aes",
"flate2",
"hmac",
"libc",
"md-5",
@@ -2523,7 +2555,7 @@ dependencies = [
[[package]]
name = "sabia-gui"
version = "0.1.0"
version = "1.0.4"
dependencies = [
"libc",
"sabia-core",
+6 -5
View File
@@ -1,12 +1,13 @@
# App desktop (Tauri 2). Fica fora do workspace principal porque exige
# as bibliotecas de WebView do sistema (WebKitGTK no Linux).
# Desktop application (Tauri 2). Kept outside the main workspace because it
# requires system WebView libraries (WebKitGTK on Linux).
[package]
name = "sabia-gui"
description = "Sabiá: interface gráfica de auditoria de Wi-Fi"
version = "0.1.0"
description = "Sabiá: graphical Wi-Fi security auditing interface"
version = "1.0.4"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/lucasrguerra/sabia"
authors = ["Sabiá contributors"]
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
rust-version = "1.80"
[workspace]
+5 -3
View File
@@ -1,12 +1,14 @@
fn main() {
println!("cargo:rerun-if-changed=../../../logo.png");
let _ = std::fs::copy("../../../logo.png", "../ui/logo.png");
#[cfg(windows)]
{
// Modo monitor e captura crua exigem administrador no Windows: o
// manifesto pede elevação (UAC) assim que o app abre.
// Monitor mode and raw frame capture require Administrator privileges on Windows:
// embed UAC elevation manifest at startup.
let manifest = include_str!("windows-app-manifest.xml");
let attrs = tauri_build::Attributes::new()
.windows_attributes(tauri_build::WindowsAttributes::new().app_manifest(manifest));
tauri_build::try_build(attrs).expect("falha no tauri-build");
tauri_build::try_build(attrs).expect("tauri-build failed");
}
#[cfg(not(windows))]
tauri_build::build();
@@ -0,0 +1,7 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Main window permissions",
"windows": ["main"],
"permissions": ["core:default", "core:window:allow-destroy", "core:window:allow-close"]
}
@@ -1 +1 @@
{}
{"default":{"identifier":"default","description":"Main window permissions","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-destroy","core:window:allow-close"]}}
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 350 KiB

+553 -28
View File
@@ -1,7 +1,7 @@
// Evita a janela de console extra no Windows em release.
// Prevents an extra console window on Windows in release builds.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use sabia_core::engine::Engine;
use sabia_core::engine::{Engine, ScanOptions};
use std::sync::Arc;
struct State(Arc<Engine>);
@@ -19,7 +19,10 @@ fn list_ifaces(state: tauri::State<'_, State>) -> Result<serde_json::Value, Stri
#[tauri::command]
fn set_monitor(state: tauri::State<'_, State>, iface: String, enable: bool) -> Result<(), String> {
state.0.set_monitor(&iface, enable).map_err(|e| e.to_string())
state
.0
.set_monitor(&iface, enable)
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -33,10 +36,465 @@ fn stop_scan(state: tauri::State<'_, State>, iface: String) {
}
#[tauri::command]
fn scan_results(state: tauri::State<'_, State>, iface: String) -> Result<serde_json::Value, String> {
fn scan_results(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.scan_results(&iface)).map_err(|e| e.to_string())
}
#[tauri::command]
fn scan_snapshot(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
let snapshot = state.0.scan_snapshot(&iface).map_err(|e| e.to_string())?;
serde_json::to_value(snapshot).map_err(|e| e.to_string())
}
#[tauri::command]
fn scan_clients(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.scan_clients(&iface)).map_err(|e| e.to_string())
}
#[tauri::command]
fn handshakes(
state: tauri::State<'_, State>,
iface: Option<String>,
) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.handshakes(iface.as_deref())).map_err(|e| e.to_string())
}
#[tauri::command]
fn export_handshake(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_handshake(&iface, &bssid, &station, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn export_all_handshakes(
state: tauri::State<'_, State>,
iface: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_all_handshakes(&iface, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_crack(
state: tauri::State<'_, State>,
iface: String,
wordlist: Option<String>,
bssid: Option<String>,
station: Option<String>,
rules: Option<bool>,
) -> Result<String, String> {
let rules = rules.unwrap_or(false);
let result = match wordlist.filter(|w| !w.trim().is_empty()) {
Some(wordlist) => state.0.start_crack(
&iface,
bssid.as_deref(),
station.as_deref(),
&wordlist,
rules,
),
None => state
.0
.start_crack_default(&iface, bssid.as_deref(), station.as_deref(), rules),
};
result.map_err(|e| e.to_string())
}
#[tauri::command]
fn start_crack_mask(
state: tauri::State<'_, State>,
iface: String,
mask: String,
bssid: Option<String>,
station: Option<String>,
max_candidates: Option<u64>,
) -> Result<String, String> {
state
.0
.start_crack_mask(
&iface,
bssid.as_deref(),
station.as_deref(),
&mask,
max_candidates.unwrap_or(50_000_000),
)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn crack_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.crack_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job não encontrado".into()),
}
}
#[tauri::command]
fn crack_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.crack_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn deauth(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
count: Option<u32>,
reason: Option<u16>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.deauth(
&iface,
&bssid,
station.as_deref(),
count.unwrap_or(5),
reason.unwrap_or(7),
)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
fn deauth_directional(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
count: Option<u32>,
reason: Option<u16>,
direction: Option<String>,
frame_kind: Option<String>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.deauth_directional(
&iface,
&bssid,
station.as_deref(),
count.unwrap_or(5),
reason.unwrap_or(7),
direction.as_deref().unwrap_or("ap"),
frame_kind.as_deref().unwrap_or("deauth"),
)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
fn start_deauth_job(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
reason: Option<u16>,
direction: Option<String>,
frame_kind: Option<String>,
interval_ms: Option<u64>,
max_secs: Option<u64>,
stop_on_handshake: Option<bool>,
) -> Result<String, String> {
state
.0
.start_deauth_job(
&iface,
&bssid,
station.as_deref(),
reason.unwrap_or(7),
direction.as_deref().unwrap_or("both"),
frame_kind.as_deref().unwrap_or("deauth"),
interval_ms.unwrap_or(120),
max_secs,
stop_on_handshake.unwrap_or(false),
)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_deauth_job(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state.0.stop_deauth_job(&id).map_err(|e| e.to_string())
}
#[tauri::command]
fn deauth_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.deauth_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job de deauth não encontrado".into()),
}
}
#[tauri::command]
fn deauth_jobs(
state: tauri::State<'_, State>,
iface: Option<String>,
) -> Result<serde_json::Value, String> {
let jobs = state.0.deauth_jobs();
let jobs: Vec<_> = match iface {
Some(iface) => jobs
.into_iter()
.filter(|j| j.iface.eq_ignore_ascii_case(&iface))
.collect(),
None => jobs,
};
serde_json::to_value(jobs).map_err(|e| e.to_string())
}
#[tauri::command]
fn capture_handshake(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: Option<String>,
seconds: Option<u64>,
) -> Result<String, String> {
state
.0
.capture_handshake(&iface, &bssid, station.as_deref(), seconds)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn report(
state: tauri::State<'_, State>,
iface: String,
) -> Result<serde_json::Value, String> {
let model = state.0.report(&iface).map_err(|e| e.to_string())?;
serde_json::to_value(model).map_err(|e| e.to_string())
}
#[tauri::command]
fn report_html(
state: tauri::State<'_, State>,
iface: String,
inline: Option<bool>,
) -> Result<String, String> {
let model = state.0.report(&iface).map_err(|e| e.to_string())?;
let options = sabia_core::report::ReportOptions {
inline_pagedjs: inline.unwrap_or(false),
..Default::default()
};
Ok(sabia_core::report::render(&model, options))
}
#[tauri::command]
fn export_hc22000(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_hc22000(&iface, &bssid, &station, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn export_all_hc22000(
state: tauri::State<'_, State>,
iface: String,
out: String,
) -> Result<serde_json::Value, String> {
let report = state
.0
.export_all_hc22000(&iface, &out)
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn pmkid_hunt(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
attempts: Option<u32>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.pmkid_hunt(&iface, &bssid, attempts.unwrap_or(5))
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn pmf_test(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
station: String,
reason: Option<u16>,
) -> Result<serde_json::Value, String> {
let report = state
.0
.pmf_test(&iface, &bssid, &station, reason.unwrap_or(7))
.map_err(|e| e.to_string())?;
serde_json::to_value(report).map_err(|e| e.to_string())
}
#[tauri::command]
fn wps_tools(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.wps_tools()).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_wps(
state: tauri::State<'_, State>,
iface: String,
bssid: String,
channel: u32,
tool: String,
pin: Option<String>,
) -> Result<String, String> {
state
.0
.start_wps(&iface, &bssid, channel, &tool, pin.as_deref())
.map_err(|e| e.to_string())
}
#[tauri::command]
fn wps_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.wps_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job WPS não encontrado".into()),
}
}
#[tauri::command]
fn wps_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.wps_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_wps(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state.0.stop_wps(&id).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_evil_twin(
state: tauri::State<'_, State>,
iface: String,
ap_iface: String,
bssid: String,
portal_port: Option<u16>,
deauth: Option<bool>,
) -> Result<String, String> {
state
.0
.start_evil_twin(
&iface,
&ap_iface,
&bssid,
portal_port.unwrap_or(8080),
deauth.unwrap_or(false),
)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn eviltwin_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
match state.0.eviltwin_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job de Evil Twin não encontrado".into()),
}
}
#[tauri::command]
fn eviltwin_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.eviltwin_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_evil_twin(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state.0.stop_evil_twin(&id).map_err(|e| e.to_string())
}
#[tauri::command]
fn start_evil_twin_enterprise(
state: tauri::State<'_, State>,
iface: String,
ap_iface: String,
bssid: String,
) -> Result<String, String> {
state
.0
.start_evil_twin_enterprise(&iface, &ap_iface, &bssid)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn eviltwin_enterprise_job(
state: tauri::State<'_, State>,
id: String,
) -> Result<serde_json::Value, String> {
match state.0.enterprise_twin_job(&id) {
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
None => Err("job de Evil Twin Enterprise não encontrado".into()),
}
}
#[tauri::command]
fn eviltwin_enterprise_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
serde_json::to_value(state.0.enterprise_twin_jobs()).map_err(|e| e.to_string())
}
#[tauri::command]
fn stop_evil_twin_enterprise(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
state
.0
.stop_evil_twin_enterprise(&id)
.map_err(|e| e.to_string())
}
#[tauri::command]
#[allow(clippy::too_many_arguments)]
fn start_scan_opts(
state: tauri::State<'_, State>,
iface: String,
channel: Option<u32>,
band: Option<String>,
hop_ms: Option<u64>,
pcap_out: Option<String>,
seconds: Option<u64>,
) -> Result<(), String> {
let options = ScanOptions {
channel,
band,
hop_ms: hop_ms.unwrap_or(350),
pcap_out: pcap_out.map(Into::into),
duration_secs: seconds,
};
state
.0
.start_scan_with(&iface, options)
.map_err(|e| e.to_string())
}
#[tauri::command]
fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
let status = state.0.update_check().map_err(|e| e.to_string())?;
@@ -48,9 +506,9 @@ fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
state.0.update_apply().map_err(|e| e.to_string())
}
/// `true` quando o processo já tem privilégio suficiente pra tudo que o
/// Sabiá faz (modo monitor, captura crua). No Windows o manifesto já
/// exige administrador antes mesmo do app abrir, então aqui é sempre `true`.
/// `true` when the process already has sufficient privileges for all
/// Sabiá operations (monitor mode, raw capture). On Windows the manifest already
/// requires administrator privileges before launch, so this is always `true`.
#[tauri::command]
fn is_elevated() -> bool {
#[cfg(unix)]
@@ -63,9 +521,9 @@ fn is_elevated() -> bool {
}
}
/// Reabre o app com privilégios de administrador (Linux/macOS). O app novo
/// encerra este quando abre; se a autorização falhar ou for cancelada, a
/// janela continua aqui e a interface mostra o motivo.
/// Relaunches the app with administrator privileges (Linux/macOS). The new instance
/// terminates this one on startup; if authorization fails or is cancelled, the
/// window stays open and the UI displays the reason.
#[tauri::command]
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -85,7 +543,10 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
let exe = elevated_exe()?;
let log = std::env::temp_dir().join(format!("sabia-elevate-{}.log", std::process::id()));
let _ = std::fs::write(&log, b"");
let out = std::fs::OpenOptions::new().append(true).open(&log).map_err(|e| e.to_string())?;
let out = std::fs::OpenOptions::new()
.append(true)
.open(&log)
.map_err(|e| e.to_string())?;
let err = out.try_clone().map_err(|e| e.to_string())?;
let pid = std::process::id().to_string();
@@ -93,7 +554,14 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
let mut cmd = {
let mut c = std::process::Command::new("pkexec");
c.arg("/usr/bin/env");
for k in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND"] {
for k in [
"DISPLAY",
"WAYLAND_DISPLAY",
"XAUTHORITY",
"XDG_RUNTIME_DIR",
"DBUS_SESSION_BUS_ADDRESS",
"GDK_BACKEND",
] {
if let Ok(v) = std::env::var(k) {
c.arg(format!("{k}={v}"));
}
@@ -104,14 +572,23 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
};
#[cfg(target_os = "macos")]
let mut cmd = {
let shell = format!("SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &", pid, exe.display());
let script = format!("do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\""));
let shell = format!(
"SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &",
pid,
exe.display()
);
let script = format!(
"do shell script \"{}\" with administrator privileges",
shell.replace('\\', "\\\\").replace('"', "\\\"")
);
let mut c = std::process::Command::new("osascript");
c.args(["-e", &script]);
c
};
cmd.stdout(out).stderr(err);
let mut child = cmd.spawn().map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
let mut child = cmd
.spawn()
.map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
std::thread::spawn(move || {
let status = child.wait();
if matches!(&status, Ok(s) if s.success()) {
@@ -122,7 +599,10 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
let msg = match status.ok().and_then(|s| s.code()) {
Some(126) => "Autorização cancelada.".to_string(),
Some(127) => "Não foi possível autenticar como administrador.".to_string(),
_ => format!("Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim()),
_ => format!(
"Não foi possível reiniciar como administrador. {}",
detail.lines().last().unwrap_or("").trim()
),
};
let _ = app.emit("elevate-failed", msg);
let _ = std::fs::remove_file(&log);
@@ -130,12 +610,15 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
Ok(())
}
/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta
/// pra dentro de um ponto de montagem FUSE do usuário, inacessível ao root:
/// usa o próprio arquivo `.AppImage` (variável `APPIMAGE`).
/// Executable path accessible to root. Inside an AppImage, `current_exe` points
/// inside a user FUSE mount that root cannot access:
/// use the `.AppImage` file itself (`APPIMAGE` environment variable).
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn elevated_exe() -> Result<std::path::PathBuf, String> {
if let Some(p) = std::env::var_os("APPIMAGE").map(std::path::PathBuf::from).filter(|p| p.is_file()) {
if let Some(p) = std::env::var_os("APPIMAGE")
.map(std::path::PathBuf::from)
.filter(|p| p.is_file())
{
return Ok(p);
}
std::env::current_exe().map_err(|e| e.to_string())
@@ -145,13 +628,19 @@ fn main() {
let engine = Arc::new(Engine::new());
tauri::Builder::default()
.manage(State(engine))
.setup(|app| {
// Quando abrimos elevados a pedido do app normal, ele fecha o app antigo.
.setup(|_app| {
#[cfg(any(target_os = "linux", target_os = "macos"))]
let app = _app;
// When launched elevated by the unprivileged instance, terminate the old process.
#[cfg(unix)]
if let Some(pid) = std::env::var("SABIA_REPLACE_PID").ok().and_then(|p| p.parse::<i32>().ok()) {
if let Some(pid) = std::env::var("SABIA_REPLACE_PID")
.ok()
.and_then(|p| p.parse::<i32>().ok())
{
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(1200));
// SAFETY: só envia SIGTERM ao processo anterior do próprio usuário.
// SAFETY: only sends SIGTERM to the previous process owned by the invoking user.
unsafe {
if libc::geteuid() == 0 {
libc::kill(pid, libc::SIGTERM);
@@ -160,10 +649,10 @@ fn main() {
});
}
// Captura crua e modo monitor exigem root: pede elevação assim
// que o app abre, sem esperar o usuário procurar um botão. Se
// a autorização falhar, a janela continua aberta e a interface
// mostra o aviso (com um botão pra tentar de novo).
// Raw capture and monitor mode require root: request elevation as soon
// as the app launches, without waiting for the user to look for a button. If
// authorization fails, the window stays open and the UI
// displays a warning banner (with a retry button).
#[cfg(any(target_os = "linux", target_os = "macos"))]
if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() {
let handle = app.handle().clone();
@@ -180,6 +669,42 @@ fn main() {
start_scan,
stop_scan,
scan_results,
scan_snapshot,
scan_clients,
handshakes,
export_handshake,
export_all_handshakes,
start_crack,
start_crack_mask,
crack_job,
crack_jobs,
deauth,
deauth_directional,
start_deauth_job,
stop_deauth_job,
deauth_job,
deauth_jobs,
capture_handshake,
report,
report_html,
export_hc22000,
export_all_hc22000,
pmkid_hunt,
pmf_test,
wps_tools,
start_wps,
wps_job,
wps_jobs,
stop_wps,
start_evil_twin,
eviltwin_job,
eviltwin_jobs,
stop_evil_twin,
start_evil_twin_enterprise,
eviltwin_enterprise_job,
eviltwin_enterprise_jobs,
stop_evil_twin_enterprise,
start_scan_opts,
update_check,
update_apply,
is_elevated,
+3 -3
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Sabia",
"version": "0.1.0",
"productName": "Sabiá",
"version": "1.0.4",
"identifier": "dev.sabia.app",
"build": {
"frontendDist": "../ui"
@@ -19,7 +19,7 @@
}
],
"security": {
"csp": "default-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; frame-src 'none'; object-src 'none'"
"csp": "default-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; frame-src 'self' data: blob:; object-src 'none'"
}
},
"bundle": {
+290 -20
View File
@@ -8,11 +8,11 @@
--text-3: #8e8e93;
--line: rgba(0, 0, 0, 0.09);
/* "canto" — cobre quente, o peito do sabiá-laranjeira */
/* "song" — warm copper, the rufous-bellied thrush breast */
--accent: #b45309;
--accent-ink: #ffffff;
--accent-soft: rgba(180, 83, 9, 0.11);
/* "sinal" — ciano, a onda que o rádio enxerga */
/* "signal" — cyan, the radio wave */
--signal: #0891b2;
--signal-soft: rgba(8, 145, 178, 0.12);
@@ -68,6 +68,7 @@
:root[data-theme="dark"] { color-scheme: dark; }
* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
margin: 0; background: var(--bg); color: var(--text);
@@ -104,23 +105,33 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
padding: 9px 10px; border-radius: 12px; transition: background .15s;
}
.dev:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
.dev[aria-current="true"] { background: var(--accent-soft); }
.dev[aria-current="true"], .dev[aria-pressed="true"] { background: var(--accent-soft); }
.dev .ico { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--surface); box-shadow: 0 0 0 1px var(--line); color: var(--text-2); }
.dev[aria-current="true"] .ico { color: var(--accent); }
.dev[aria-current="true"] .ico, .dev[aria-pressed="true"] .ico { color: var(--accent); }
.dev .t { display: block; font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dev .s { display: block; font-size: 11.5px; color: var(--text-2); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.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; }
.iface-pick { display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding: 0 10px 4px; }
.iface-pick .select { width: 100%; min-width: 0; }
.iface-pick .pill { align-self: flex-start; }
.side-foot { margin-top: auto; padding: 14px 4px 4px; font-size: 11.5px; color: var(--text-3); display: grid; gap: 8px; }
.foot-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.side-foot button { background: transparent; border: 0; color: var(--text-3); cursor: pointer; }
.side-foot .foot-row > button:not(.icon-btn) { 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); }
.icon-btn { width: 30px; height: 30px; border-radius: 9px; display: inline-grid; place-items: center; padding: 0; }
.icon-btn:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
.theme-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); }
.theme-switch button { border: 0; background: transparent; cursor: pointer; padding: 4px 9px; border-radius: 999px; font-size: 11.5px; color: var(--text-2); }
.theme-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .18); font-weight: 600; }
.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); }
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
/* -------------------------------------------------------- topo + linha de sinal */
/* -------------------------------------------------------- topbar + signal line */
.topbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.topbar h1 { font-size: 21px; }
.topbar .sub { color: var(--text-2); font-size: 13px; margin-top: 2px; }
@@ -130,11 +141,14 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.wave.scanning path.live { opacity: 1; }
.wave.scanning path.flat { opacity: 0; }
/* ------------------------------------------------------------ aviso */
/* ------------------------------------------------------------ notice banner */
.notice { display: flex; gap: 10px; background: var(--surface); border-radius: 12px; padding: 12px 14px; box-shadow: var(--shadow); font-size: 13px; color: var(--text-2); margin-bottom: 20px; align-items: flex-start; }
.notice svg { flex: none; margin-top: 2px; color: var(--warn); }
.notice.warn { background: var(--warn-soft); } .notice.warn svg { color: var(--warn); }
.notice.bad { background: var(--bad-soft); } .notice.bad svg { color: var(--bad); }
.notice b { color: var(--text); }
/* ------------------------------------------------------------ controles */
/* ------------------------------------------------------------ controls */
.controls { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); padding: 16px 18px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 22px; }
.controls .group { display: flex; align-items: center; gap: 8px; }
.pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); }
@@ -152,24 +166,58 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.btn.live { background: var(--bad); color: #fff; }
.btn.live:hover { filter: brightness(1.06); }
/* ------------------------------------------------------------ tabela */
.result-head { display: flex; align-items: baseline; justify-content: space-between; margin: 4px 2px 10px; }
/* ------------------------------------------------------------ table */
.result-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 4px 2px 10px; }
.result-summary { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.result-head strong { font-size: 15px; }
.result-head span { color: var(--text-3); font-size: 12.5px; }
.result-summary span { color: var(--text-3); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.result-tools { display: flex; align-items: center; gap: 8px; }
.result-tools .filter { border: 0; border-radius: 10px; padding: 7px 12px; width: 210px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; font-size: 12.5px; }
.result-tools .filter::placeholder { color: var(--text-3); }
.result-tools .select { width: auto; padding: 7px 10px; border-radius: 10px; font-size: 12.5px; }
.ap-card { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
.net-card { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; }
thead th { text-align: left; padding: 11px 14px; font-size: 10.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: .06em; font-weight: 600; border-bottom: 1px solid var(--line); }
tbody td { padding: 10px 14px; font-size: 13px; border-bottom: 1px solid var(--line); vertical-align: middle; }
tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover { background: var(--surface-2); }
td.bssid { font-family: var(--mono); font-size: 12px; color: var(--text-2); }
td.ssid { font-weight: 600; }
td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
.sec-tag { font-size: 11.5px; font-weight: 600; padding: 2px 8px; border-radius: 999px; }
.sec-aberta { background: var(--bad-soft); color: var(--bad); }
.sec-wep { background: var(--warn-soft); color: var(--warn); }
.sec-wpa, .sec-wpa2, .sec-wpa3 { background: var(--ok-soft); color: var(--ok); }
/* network grouped by ESSID; each BSSID renders as an expandable row */
.caret-cell { width: 34px; padding-left: 10px; padding-right: 0; }
.expander { width: 26px; height: 26px; border: 0; border-radius: 8px; background: transparent; color: var(--text-3); cursor: pointer; display: inline-grid; place-items: center; padding: 0; }
.expander:hover { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }
.expander svg { transition: transform .18s; }
.net-row.open .expander svg { transform: rotate(90deg); }
.net-row.open > td { background: color-mix(in srgb, var(--text) 3%, transparent); }
.net-name .ssid { display: block; font-weight: 600; line-height: 1.25; }
.net-name .ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
.net-sub { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; }
.flag { font-size: 10px; font-weight: 700; letter-spacing: .03em; padding: 1px 6px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); white-space: nowrap; }
.flag-warn { background: var(--warn-soft); color: var(--warn); }
.net-sec .sec-tag + .sec-tag { margin-left: 4px; }
.net-chan .chans { font-variant-numeric: tabular-nums; font-weight: 600; }
.net-band { display: block; font-size: 10.5px; color: var(--text-3); }
.net-signal, .net-clients { white-space: nowrap; font-variant-numeric: tabular-nums; }
.net-clients { color: var(--text-2); }
.detail-row td { padding: 4px 14px 10px 46px; background: var(--surface-2); }
.detail-row:hover { background: transparent; }
.bssid-list { display: grid; gap: 1px; }
.bssid-item { display: grid; grid-template-columns: minmax(150px, 1.4fr) 132px 112px minmax(92px, .8fr) 84px 1fr; gap: 10px; align-items: center; padding: 6px 8px; border-radius: 8px; font-size: 12.5px; }
.bssid-item:hover { background: color-mix(in srgb, var(--text) 4%, transparent); }
.bssid { color: var(--text-2); font-size: 11.5px; }
.bssid-meta, .bssid-clients { color: var(--text-3); font-size: 12px; }
.bssid-flags { display: flex; gap: 4px; justify-content: flex-end; flex-wrap: wrap; }
.sec-tag { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
.sec-open { background: var(--bad-soft); color: var(--bad); }
.sec-wep, .sec-wpa { background: var(--warn-soft); color: var(--warn); }
.sec-wpa2, .sec-wpa3, .sec-owe { background: var(--ok-soft); color: var(--ok); }
.sec-eap { background: var(--signal-soft); color: var(--signal); }
.signal-bars { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 14px; vertical-align: middle; margin-right: 6px; }
.signal-bars i { width: 3px; background: var(--line); border-radius: 1px; display: block; }
.signal-bars i.on { background: var(--signal); }
@@ -179,11 +227,233 @@ td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
.empty-state h2 { font-size: 17px; color: var(--text); margin-bottom: 6px; }
.empty-state p { font-size: 13.5px; max-width: 360px; margin: 0 auto; }
/* ------------------------------------------------------------ responsivo */
/* ------------------------------------------------------------ settings */
dialog { border: 0; border-radius: 20px; padding: 24px; width: min(420px, calc(100vw - 32px)); background: var(--surface); color: var(--text); box-shadow: 0 30px 80px rgba(0, 0, 0, .35); }
dialog::backdrop { background: rgba(0, 0, 0, .35); backdrop-filter: blur(6px); }
dialog h2 { font-size: 20px; margin-bottom: 16px; }
dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; margin: 22px 0 0; }
.field { display: grid; gap: 6px; margin: 12px 0; }
.field-label { font-size: 12px; color: var(--text-2); font-weight: 600; }
.select { width: 100%; border: 0; border-radius: 12px; padding: 9px 14px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; cursor: pointer; }
.select option { background-color: var(--surface); color: var(--text); }
/* ------------------------------------------------------------ target details */
.ssid-btn { display: block; border: 0; padding: 0; background: transparent; color: inherit; font: inherit; font-weight: 600; text-align: left; cursor: pointer; line-height: 1.25; }
.ssid-btn:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
.bssid-item { cursor: pointer; }
.bssid-item:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }
.target-dlg { width: min(980px, calc(100vw - 32px)); max-height: min(90vh, 900px); padding: 0; overflow: hidden; display: flex; flex-direction: column; }
.target-dlg:not([open]) { display: none; }
.target-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 22px 24px 12px; }
.target-head h2 { font-size: 22px; line-height: 1.2; }
.target-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 8px; color: var(--text-2); font-size: 12.5px; margin-top: 5px; }
.target-sub .mono { color: var(--text-2); font-size: 12px; }
.target-sub .sec-tag { margin-left: 2px; }
.dot-sep { color: var(--text-3); }
.bssid-select { width: auto; max-width: 320px; padding: 4px 8px; border-radius: 8px; font-size: 11.5px; }
.tabs { display: flex; gap: 2px; padding: 0 20px; border-bottom: 1px solid var(--line); }
.tabs button { border: 0; background: transparent; color: var(--text-2); padding: 9px 12px 11px; font-size: 13px; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
.tabs button:hover { color: var(--text); }
.tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); }
.tab-count { display: inline-block; min-width: 18px; padding: 0 5px; margin-left: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text) 9%, transparent); color: var(--text-2); font-size: 10.5px; line-height: 17px; text-align: center; }
.tab-panel { padding: 18px 24px 26px; overflow-y: auto; min-height: 0; }
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.detail-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; }
.detail-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); margin-bottom: 10px; }
.detail-wide { margin-top: 12px; }
.kv { display: grid; grid-template-columns: minmax(96px, auto) minmax(0, 1fr); gap: 6px 12px; margin: 0; font-size: 12.5px; }
.kv dt { color: var(--text-3); }
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.detail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.btn.small { padding: 6px 12px; font-size: 12px; }
.risk { border-radius: 12px; padding: 10px 13px; font-size: 12.5px; margin-bottom: 12px; border: 1px solid transparent; }
.risk-bad { background: var(--bad-soft); color: var(--bad); }
.risk-warn { background: var(--warn-soft); color: var(--warn); }
.risk-info { background: var(--signal-soft); color: var(--signal); }
.risk strong { font-weight: 700; }
.flag-ok { background: var(--ok-soft); color: var(--ok); }
.flag-bad { background: var(--bad-soft); color: var(--bad); }
.clients-card { margin-top: 2px; overflow-x: auto; }
.mini-table { width: 100%; border-collapse: collapse; }
.mini-table .row-actions { text-align: right; white-space: nowrap; }
.mini-table th { padding: 9px 12px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); text-align: left; border-bottom: 1px solid var(--line); }
.mini-table td { padding: 9px 12px; font-size: 12.5px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.mini-table tr:last-child td { border-bottom: 0; }
.mini-table .nowrap { white-space: nowrap; font-variant-numeric: tabular-nums; }
.mini-table .probes { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.mini-table .device-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.clients-card .mini-table { table-layout: fixed; }
.clients-card .mini-table th:nth-child(1) { width: 20%; }
.clients-card .mini-table th:nth-child(2) { width: 18%; }
.clients-card .mini-table th:nth-child(3) { width: 12%; }
.clients-card .mini-table th:nth-child(4) { width: 7%; }
.clients-card .mini-table th:nth-child(5) { width: 10%; }
.clients-card .mini-table th:nth-child(6) { width: 12%; }
.clients-card .mini-table th:nth-child(7) { width: 21%; }
.clients-card .row-actions .btn { padding: 6px 10px; }
.clients-card .client-name, .clients-card .client-sub { max-width: 100%; }
.client-name { font-weight: 600; color: var(--text); max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.client-sub { color: var(--text-3); font-size: 10.5px; max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.empty-inline { text-align: center; padding: 46px 20px; color: var(--text-2); background: var(--surface-2); border-radius: 14px; }
.empty-inline strong { color: var(--text); display: block; margin-bottom: 4px; }
.empty-inline p { margin: 0; font-size: 13px; }
.auth-check { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; margin: 4px 0 14px; }
.auth-check input { width: 16px; height: 16px; accent-color: var(--accent); }
/* CSS multi-column layout instead of grid: attack cards have very different
heights (Deauth/Handshake are tall, PMF/WPS are short). In a standard
2-column grid, the shorter card in a row left a large empty gap underneath.
Multi-column masonry flow lets each card take only the height it needs
with the next card stacking right below. */
.attack-grid { column-count: 2; column-gap: 12px; }
.attack-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; min-width: 0; break-inside: avoid; margin: 0 0 12px; display: inline-block; width: 100%; }
.attack-deauth, .attack-wide { column-span: all; }
.attack-deauth .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.attack-deauth .detail-actions { align-items: center; }
.attack-card h3 { font-size: 14px; margin-bottom: 5px; }
.attack-card .hint { margin: 6px 0 10px; }
.attack-card .btn { margin-top: 2px; }
.field-row { display: grid; grid-template-columns: 1fr 1.4fr; gap: 10px; }
.field { display: grid; gap: 5px; margin: 9px 0; }
.field-label { font-size: 11.5px; color: var(--text-2); font-weight: 600; }
.input { width: 100%; border: 1px solid transparent; border-radius: 10px; padding: 8px 11px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; font-size: 12.5px; }
.input:focus { outline: 2px solid var(--signal); outline-offset: 1px; }
.hint { color: var(--text-3); font-size: 12px; margin: 0; line-height: 1.4; }
.hint.bad { color: var(--bad); }
.hint code { background: color-mix(in srgb, var(--text) 7%, transparent); padding: 1px 5px; border-radius: 5px; font-size: 11.5px; }
.hs-list { display: grid; gap: 6px; margin-bottom: 8px; }
.hs-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; font-size: 12px; }
.hs-main { min-width: 0; display: grid; gap: 2px; }
.hs-meta { color: var(--text-3); font-size: 11px; }
.hs-flags { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
.crack-box { margin-top: 10px; padding: 9px 11px; border-radius: 10px; background: var(--surface); border: 1px solid var(--line); font-size: 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
.crack-ok { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.crack-box .mono { margin-right: 4px; }
.crack-head { display: flex; align-items: center; gap: 8px; width: 100%; color: var(--text-2); }
.crack-bar { width: 100%; height: 4px; border-radius: 999px; background: var(--line); overflow: hidden; }
.crack-bar i { display: block; width: 35%; height: 100%; border-radius: 999px; background: var(--accent); animation: crack-slide 1.1s ease-in-out infinite; }
@keyframes crack-slide { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
/* ------------------------------------------------------------ responsive */
@media (max-width: 760px) {
.app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
.sidebar { border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; align-items: center; overflow-x: auto; }
.brand { padding: 4px 10px; }
.side-title, .side-foot { display: none; }
.list { display: flex; }
.result-tools { width: 100%; }
.result-tools .filter { flex: 1; width: auto; }
.net-table { min-width: 560px; }
.bssid-item { grid-template-columns: 1fr auto; }
.bssid-meta, .bssid-sig, .bssid-flags { grid-column: 1 / -1; }
.bssid-flags { justify-content: flex-start; }
.target-dlg { width: calc(100vw - 16px); max-height: 94vh; }
.target-head { padding: 16px 16px 10px; }
.tabs { padding: 0 10px; overflow-x: auto; }
.tab-panel { padding: 14px 14px 20px; }
.detail-grid { grid-template-columns: 1fr; }
.attack-grid { column-count: 1; }
.field-row { grid-template-columns: 1fr; }
.detail-card, .attack-card { padding: 12px; }
}
/* --------------------------------------------------- channel analysis */
.top-actions { display: flex; align-items: center; gap: 10px; }
.view-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 7%, transparent); }
.view-switch button { border: 0; border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; color: var(--text-2); background: transparent; cursor: pointer; }
.view-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
.alert-strip { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 0 2px 10px; }
.alert-item { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); background: var(--surface); border-radius: 999px; padding: 5px 11px; font-size: 12px; cursor: pointer; }
.alert-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--warn); }
.alert-high .alert-dot { background: var(--bad); }
.alert-low .alert-dot { background: var(--text-3); }
.alert-title { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.alert-more { color: var(--text-3); font-size: 11.5px; padding: 0 4px; }
.channel-card { padding: 16px; }
.channel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
.band-switch { display: inline-flex; gap: 4px; }
.band-switch button { border: 1px solid transparent; background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text-2); border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; cursor: pointer; }
.band-switch button[aria-pressed="true"] { background: var(--signal-soft); color: var(--signal); border-color: color-mix(in srgb, var(--signal) 35%, transparent); }
.sp-legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--text-3); font-size: 11px; }
.sp-legend span { display: inline-flex; align-items: center; gap: 5px; }
.sp-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.spectrum { width: 100%; height: auto; display: block; background: color-mix(in srgb, var(--text) 3%, transparent); border-radius: 12px; }
.sp-grid { stroke: var(--line); stroke-width: 1; }
.sp-axis { stroke: color-mix(in srgb, var(--text) 22%, transparent); stroke-width: 1; }
.sp-db, .sp-chan { fill: var(--text-3); font-size: 9px; font-family: var(--mono); }
.sp-name { fill: var(--text-2); font-size: 9px; font-weight: 600; }
.sp-lobe path { transition: fill-opacity .15s; }
.sp-lobe:hover path { fill-opacity: .38; }
.channel-grid { display: grid; grid-template-columns: 190px 1fr; gap: 18px; margin-top: 14px; }
.channel-grid h3 { font-size: 12px; color: var(--text-2); margin-bottom: 8px; }
.chan-best-num { font-size: 44px; font-weight: 700; line-height: 1; color: var(--signal); margin: 0 0 6px; }
.chan-list { display: grid; gap: 6px; }
.chan-list h3 { margin: 0 0 2px; }
.chan-row { display: grid; grid-template-columns: 46px 1fr 48px 128px; align-items: center; gap: 8px; font-size: 12px; }
.chan-row .chan-n { font-weight: 600; display: inline-flex; align-items: center; gap: 4px; }
.chan-bar { height: 7px; border-radius: 999px; background: var(--line); overflow: hidden; }
.chan-bar i { display: block; height: 100%; border-radius: 999px; background: var(--ok); }
.chan-bar i.diff-mid { background: var(--warn); }
.chan-bar i.diff-high { background: var(--bad); }
.chan-score { font-family: var(--mono); font-size: 11px; color: var(--text-2); text-align: right; }
.chan-aps { color: var(--text-3); font-size: 11px; white-space: nowrap; }
.chan-best .chan-n, .chan-best .chan-aps { color: var(--signal); }
/* ------------------------------------------------- spectrum health (SMART) */
.chan-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; margin: 2px 0 16px; }
.chan-tile { background: var(--surface-2); border-radius: 14px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; box-shadow: 0 0 0 1px var(--line); }
.chan-tile small { color: var(--text-3); font-size: 12px; }
.chan-tile b { font-size: 18px; font-variant-numeric: tabular-nums; }
.chan-tile.status.ok { background: var(--ok-soft); } .chan-tile.status.ok b { color: var(--ok); }
.chan-tile.status.warn { background: var(--warn-soft); } .chan-tile.status.warn b { color: var(--warn); }
.chan-tile.status.bad { background: var(--bad-soft); } .chan-tile.status.bad b { color: var(--bad); }
.chan-tile b span { font-weight: 500; opacity: .8; font-size: 13px; margin-left: 4px; }
.chan-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); margin-right: 5px; flex: none; }
.chan-dot.warn { background: var(--warn); }
.chan-dot.bad { background: var(--bad); }
.chan-attrs { margin-top: 4px; }
.chan-attrs summary { cursor: pointer; font-size: 12.5px; font-weight: 600; color: var(--text-2); padding: 6px 0; list-style: none; }
.chan-attrs summary::-webkit-details-marker { display: none; }
.chan-attrs summary::before { content: '▸ '; display: inline-block; width: 12px; }
.chan-attrs[open] summary::before { content: '▾ '; }
.chan-table .dim { color: var(--text-3); font-size: 11.5px; }
.chan-table tr.chan-best-row td { color: var(--signal); font-weight: 600; }
.mini-tag { font-size: 8.5px; padding: 1px 4px; border-radius: 4px; background: var(--warn-soft); color: var(--warn); }
/* ------------------------------------------------ clients and timeline */
.spark { display: inline-block; width: 62px; height: 16px; vertical-align: middle; margin-left: 6px; }
.spark path { fill: none; stroke: var(--signal); stroke-width: 1.4; }
.spark-big { display: block; width: 100%; height: 70px; margin: 10px 0 0; }
.timeline-card { margin-top: 12px; }
.timeline-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
.timeline { list-style: none; padding: 0; margin: 8px 0; display: grid; gap: 5px; max-height: 260px; overflow: auto; }
.timeline li { display: grid; grid-template-columns: 84px 112px 1fr; align-items: baseline; gap: 8px; font-size: 12px; border-bottom: 1px solid var(--line); padding: 5px 2px; }
.timeline li > .mono:first-child { white-space: nowrap; }
.tl-kind { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-3); }
.tl-deauth { color: var(--bad); }
.tl-associate { color: var(--ok); }
.tl-probe { color: var(--signal); }
.tl-detail { color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* ------------------------------------------------------------- WPS and report */
.term { margin: 10px 0 0; padding: 10px 12px; border-radius: 10px; background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text-2); font: 11.5px/1.5 var(--mono); white-space: pre-wrap; max-height: 180px; overflow: auto; }
.report-dlg { width: min(1060px, 96vw); height: 92vh; max-height: 92vh; padding: 0; display: flex; flex-direction: column; overflow: hidden; }
.report-dlg:not([open]) { display: none; }
.report-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: 16px 18px 12px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.report-head h2 { font-size: 17px; margin: 0 0 2px; }
.report-tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.report-tools .mono { font-size: 11.5px; color: var(--text-2); min-width: 38px; text-align: center; }
.report-view { flex: 1; overflow: auto; background: color-mix(in srgb, var(--text) 8%, var(--bg)); padding: 16px; }
.report-canvas { margin: 0 auto; transform-origin: top center; background: #fff; box-shadow: 0 6px 30px rgba(0, 0, 0, .18); }
.report-canvas iframe { border: 0; display: block; background: #fff; }
@media (max-width: 760px) {
.top-actions { gap: 6px; }
.channel-grid { grid-template-columns: 1fr; }
.chan-row { grid-template-columns: 42px 1fr 44px; }
.chan-aps { grid-column: 2 / -1; }
.report-dlg { width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; }
.report-view { padding: 8px; }
}
+2221 -54
View File
File diff suppressed because it is too large. Load diff
+475 -6
View File
@@ -1,9 +1,13 @@
// Dicionário português -> inglês da interface. {n} marca um valor variável.
// Portuguese -> English UI dictionary. {n} marks a variable placeholder.
export const EN = {
// index.html / sidebar
'auditoria de Wi-Fi': 'Wi-Fi auditing',
'Interface': 'Interface',
'Interface Wi-Fi': 'Wi-Fi interface',
'Interfaces': 'Interfaces',
'nenhuma interface Wi-Fi encontrada': 'no Wi-Fi interface found',
'APs, clientes e ataques': 'APs, clients and attacks',
'espectro e saúde': 'spectrum and health',
'checar atualização': 'check for update',
'checando…': 'checking…',
'atualizar para {n}': 'update to {n}',
@@ -15,7 +19,7 @@ export const EN = {
'modo monitor': 'monitor mode',
'modo gerenciado': 'managed mode',
// elevação / avisos
// elevation / notices
'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',
@@ -23,14 +27,19 @@ export const EN = {
'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
// controls
'ativar modo monitor': 'enable monitor mode',
'desativar modo monitor': 'disable monitor mode',
'iniciar captura': 'start capture',
'parar captura': 'stop capture',
// resultados
// results
'Redes': 'Networks',
'Redes vistas': 'Networks seen',
'Linha do tempo': 'Timeline',
'Linha do tempo ·': 'Timeline ·',
'fechar': 'close',
'motivo {n}': 'reason {n}',
'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',
@@ -41,16 +50,44 @@ export const EN = {
'oculta': 'hidden',
'Rede': 'Network',
'Canal': 'Channel',
'Canais': 'Channels',
'Banda': 'Band',
'Bandas': 'Bands',
'Nome': 'Name',
'Sinal': 'Signal',
'Segurança': 'Security',
'Clientes': 'Clients',
'filtrar por SSID ou BSSID': 'filter by SSID or BSSID',
'Filtrar redes': 'Filter networks',
'Ordenar por': 'Sort by',
'expandir': 'expand',
'recolher': 'collapse',
'PMF parcial': 'partial PMF',
'canal {n}': 'channel {n}',
'canal {n} · {n}': 'channel {n} · {n}',
'1 cliente': '1 client',
'{n} clientes': '{n} clients',
'{n} de {n} redes': '{n} of {n} networks',
'{n} redes · {n} BSSIDs · {n} clientes': '{n} networks · {n} BSSIDs · {n} clients',
'Nenhuma rede corresponde ao filtro': 'No network matches the filter',
'Ajuste a busca ou limpe o campo de filtro.': 'Adjust the search or clear the filter.',
// erros de UI
// UI errors
'Não foi possível trocar o modo:': 'Could not change the mode:',
'Não foi possível iniciar a captura:': 'Could not start the capture:',
'Não foi possível retomar a captura: {n}': 'Could not resume the capture: {n}',
'Retomando a captura e localizando a rede…': 'Resuming the capture and locating the network…',
'A rede não reapareceu na varredura a tempo. Tente de novo.': 'The network did not reappear in the scan in time. Try again.',
'A captura parou:': 'The capture stopped:',
'erro desconhecido': 'unknown error',
'rota desconhecida': 'unknown route',
// seletor de idioma
// language selector / settings
'Configurações': 'Settings',
'Tema': 'Theme',
'Claro': 'Light',
'Escuro': 'Dark',
'Fechar': 'Close',
'Idioma': 'Language',
'Auto': 'Auto',
'Português': 'Portuguese',
@@ -95,4 +132,436 @@ export const EN = {
'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}',
// target details and attacks
'não': 'no',
'não anunciado': 'not advertised',
'quebrável': 'crackable',
'Nenhum handshake capturado para este BSSID.': 'No handshake captured for this BSSID.',
'Rede aberta.': 'Open network.',
'O tráfego não é cifrado pelo Wi-Fi; qualquer cliente pode se associar.': 'Traffic is not encrypted over Wi-Fi; any client can associate.',
'A cifra é quebrável; capture IVs na aba Ataques.': 'The cipher is breakable; capture IVs in the Attacks tab.',
'O AP anuncia configuração travada.': 'The AP advertises configuration locked.',
'PIN pode estar acessível; verifique o lock.': 'The PIN may be accessible; check the lock.',
'Desautenticação e captura de handshake podem ser ignoradas pelos clientes.': 'Deauthentication and handshake capture may be ignored by clients.',
'Sem PMF obrigatório.': 'No mandatory PMF.',
'Desautenticação tende a funcionar e facilita a captura do handshake.': 'Deauthentication tends to work and makes handshake capture easier.',
'País': 'Country',
'Último visto': 'Last seen',
'Primeiro visto': 'First seen',
'Frequência': 'Frequency',
'Largura': 'Width',
'Padrão': 'Standard',
'Sinal máximo': 'Max signal',
'Intervalo de beacon': 'Beacon interval',
'Quadros de dados': 'Data frames',
'Versão WPS': 'WPS version',
'Vistos na rede': 'Seen on network',
'Quebráveis': 'Crackable',
'Número do modelo': 'Model number',
'Nome do dispositivo': 'Device name',
'Serial': 'Serial',
'Identificação': 'Identity',
'Rádio': 'Radio',
'sim': 'yes',
'Nome exibido': 'Display name',
'Hostname': 'Hostname',
'Sistema': 'OS',
'IP': 'IP',
'Associado a': 'Associated to',
'Atual': 'Current',
'Máximo': 'Maximum',
'Sem amostras de sinal suficientes ainda.': 'Not enough signal samples yet.',
'Linha do tempo · eventos mais recentes primeiro.': 'Timeline · most recent events first.',
'Cliente · {n}': 'Client · {n}',
'A captura parou sozinha:': 'The capture stopped on its own:',
'A placa não está capturando nada': 'The adapter is not capturing anything',
'Nenhum quadro recebido depois de alguns segundos saltando canais — esta placa pode aceitar ativar o modo monitor sem conseguir capturar de verdade (comum em Wi-Fi interna de notebook). Tente um adaptador USB dedicado a modo monitor/injeção.':
'No frame received after several seconds of channel hopping — this adapter may accept turning monitor mode on without actually being able to capture (common with a laptop\'s built-in Wi-Fi). Try a USB adapter dedicated to monitor mode/injection.',
'motivo desconhecido — confira se o adaptador ainda está conectado.':
'unknown reason — check whether the adapter is still connected.',
'Clientes e handshake': 'Clients and handshake',
'Nenhum cliente visto.': 'No client seen.',
'Associe ou gere tráfego na rede para que o Sabiá identifique estações.': 'Associate or generate traffic on the network so Sabiá can identify stations.',
'senha encontrada': 'password found',
'concluído': 'done',
'quebrando': 'cracking',
'senhas testadas': 'passwords tried',
'{n} senhas testadas': '{n} passwords tried',
'{n} tentativas · {n} ms': '{n} attempts · {n} ms',
'Senha não encontrada no dicionário ({n} tentativas).': 'Password not found in the wordlist ({n} attempts).',
'· quebrável': '· crackable',
'não especificado': 'unspecified',
'autenticação expirada': 'authentication expired',
'saindo': 'leaving',
'inativo': 'inactive',
'AP sem recursos': 'AP out of resources',
'classe 2': 'class 2',
'classe 3': 'class 3',
'{n} handshake(s) visto(s); {n} quebrável(is).': '{n} handshake(s) seen; {n} crackable.',
'Nenhum handshake capturado ainda. Desautentique um cliente para estimular a reassociação.': 'No handshake captured yet. Deauthenticate a client to trigger reassociation.',
'Captura WEP': 'WEP capture',
'WEP não tem handshake: o ataque coleta IVs em um .pcap. Reinicie a captura gravando o arquivo e gere tráfego com desautenticação. A captura atual é reiniciada e a tabela começa vazia.': 'WEP has no handshake: the attack collects IVs into a .pcap. Restart the capture writing the file and generate traffic with deauthentication. The current capture is restarted and the table starts empty.',
'Arquivo pcap': 'pcap file',
'Reiniciar captura com pcap': 'Restart capture to pcap',
'Gerar tráfego': 'Generate traffic',
'Copiar comando': 'Copy command',
'Esta rede não usa WEP.': 'This network does not use WEP.',
'Somente em redes próprias ou com autorização escrita.': 'Only on your own networks or with written authorization.',
'Cada ação fica registrada na trilha de auditoria local, com interface, BSSID e estação.': 'Every action is logged in the local audit trail with interface, BSSID and station.',
'Tenho autorização para testar esta rede': 'I have authorization to test this network',
'Desautenticação': 'Deauthentication',
'Expulsa uma estação (ou todas) para forçar reassociação e captura de handshake.': 'Kicks a station (or all) to force reassociation and handshake capture.',
'Enviar desautenticação': 'Send deauthentication',
'Forçar handshake': 'Force handshake',
'Dicionário': 'Wordlist',
'Rede oculta': 'Hidden network',
'job não encontrado': 'job not found',
'{n} quadro(s) enviados para {n} (motivo {n}).': '{n} frame(s) sent to {n} (reason {n}).',
'Selecione um handshake.': 'Select a handshake.',
'Informe o dicionário.': 'Enter the wordlist.',
'Informe o caminho do .pcap.': 'Enter the .pcap path.',
'Força bruta por máscara — sem dicionário, testa toda combinação do padrão.':
'Mask brute force — no wordlist, tests every combination of the pattern.',
'dígito,': 'digit,',
'minúscula,': 'lowercase,',
'maiúscula,': 'uppercase,',
'símbolo,': 'symbol,',
'qualquer um; o resto é literal.': 'any of those; the rest is literal.',
'Máscara': 'Mask',
'Força bruta por máscara': 'Mask brute force',
'Informe uma máscara (ex.: ?d?d?d?d?d?d?d?d).': 'Enter a mask (e.g. ?d?d?d?d?d?d?d?d).',
'job não iniciado': 'job not started',
'Captura gravando em {n}. Gere tráfego e aguarde IVs suficientes.': 'Capture writing to {n}. Generate traffic and wait for enough IVs.',
'Visão geral': 'Overview',
'Ver detalhes': 'View details',
'Ver clientes': 'View clients',
'Abrir ataques': 'Open attacks',
'Todos (broadcast)': 'All (broadcast)',
'associado': 'associated',
'sondando': 'probing',
'visto': 'seen',
'Alvo': 'Target',
'Handshake': 'Handshake',
'Salvar pcap em': 'Save pcap to',
'Exportar selecionado': 'Export selected',
'Exportar todos': 'Export all',
'Quebrar senha': 'Crack password',
'completo': 'complete',
'Ataques': 'Attacks',
'Cliente alvo': 'Target client',
'Ações': 'Actions',
'Atacar': 'Attack',
'Cliente não associado a este AP.': 'Client not associated with this AP.',
'Somente o cliente selecionado será desautenticado.': 'Only the selected client will be deauthenticated.',
'Todos os clientes associados (broadcast).': 'All associated clients (broadcast).',
'Fabricante': 'Manufacturer',
'Modelo': 'Model',
'Oculta': 'Hidden',
'Modo': 'Mode',
'Cifra': 'Cipher',
'Privacidade': 'Privacy',
'WPS travado': 'WPS locked',
'Associados': 'Associated',
'Senha': 'Password',
'Quadro': 'Frame',
'Quadros': 'Frames',
'ambas': 'both',
'Tentativas': 'Attempts',
'Testar PMF': 'Test PMF',
'em execução': 'running',
'iniciando': 'starting',
'erro': 'error',
'parado': 'stopped',
'Status: {n}': 'Status: {n}',
'tamanho inválido': 'invalid length',
'· última: {n}': '· last: {n}',
'Clona o AP (aberto) numa segunda interface e sobe um portal pedindo a senha de novo. A senha só é aceita se bater de verdade com o handshake já capturado — não é um "aceita qualquer coisa".':
'Clones the AP (open) on a second interface and shows a portal asking for the password again. The password is only accepted if it truly matches the already-captured handshake — it is not an "accept anything" page.',
'Exige um handshake quebrável já capturado deste AP (aba Handshake). Precisa de':
'Requires a crackable handshake already captured for this AP (Handshake tab). Needs',
'instalado e de uma': 'installed and a',
'interface Wi-Fi em modo AP, diferente da que está capturando.':
'Wi-Fi interface in AP mode, different from the one capturing.',
'Iniciar Evil Twin': 'Start Evil Twin',
'Informe a interface do clone (uma segunda placa).':
'Enter the clone interface (a second adapter).',
'a interface do clone precisa ser diferente da que está capturando':
'the clone interface must be different from the one capturing',
'esse BSSID ainda não apareceu nesta sessão de captura':
'that BSSID has not appeared in this capture session yet',
'rede oculta: sem SSID não dá pra clonar': 'hidden network: no SSID, cannot clone',
'handshake sem dados suficientes pra conferir senha':
'handshake does not have enough data to verify a password',
'hostapd não está instalado (sudo apt install hostapd) — necessário pro Evil Twin':
'hostapd is not installed (sudo apt install hostapd) — needed for Evil Twin',
'{n} · {n} credencial(is) capturada(s){n}': '{n} · {n} credential(s) captured{n}',
'Clona o AP numa segunda interface e deixa o hostapd-wpe aceitar a autenticação EAP de qualquer cliente. A credencial sai sozinha, como parte do próprio 802.1X — sem portal, sem ação humana.':
'Clones the AP on a second interface and lets hostapd-wpe accept the EAP authentication from any client. The credential comes out on its own, as part of 802.1X itself — no portal, no human action.',
'Rede Enterprise detectada (AKM 802.1X) — aqui a credencial é enviada automaticamente pelo cliente durante o handshake EAP, não precisa de portal. Exige':
'Enterprise network detected (AKM 802.1X) — here the credential is sent automatically by the client during the EAP handshake, no portal needed. Requires',
'instalado (não vem com o Sabiá: é um patch no build do hostapd —':
'installed (not bundled with Sabiá: it is a patch on the hostapd build —',
') e uma': ') and a',
'interface Wi-Fi em modo AP.': 'Wi-Fi interface in AP mode.',
'Iniciar Evil Twin Enterprise': 'Start Enterprise Evil Twin',
'Limitação honesta: o certificado TLS do clone é autoassinado. Dispositivo que valida a cadeia de certificado da rede (comum em perfil corporativo/MDM atualizado) vai recusar ou avisar o usuário — essa é a defesa que deveria existir. Funciona contra configuração sem essa validação.':
'Honest limitation: the clone\'s TLS certificate is self-signed. A device that validates the network\'s certificate chain (common on an updated corporate/MDM profile) will refuse or warn the user — that is the defense that is supposed to exist. It works against configurations without that validation.',
'essa rede não é WPA2/WPA3-Enterprise (802.1X) — pra rede com senha única (PSK), use o Evil Twin com portal cativo':
'this network is not WPA2/WPA3-Enterprise (802.1X) — for a single-password (PSK) network, use the Evil Twin with captive portal',
'hostapd-wpe não está instalado — não vem com o Sabiá (é um patch sobre o build do hostapd, não um binário pequeno); instale pelo repositório do Kali ou compile https://github.com/aircrack-ng/hostapd-wpe':
'hostapd-wpe is not installed — not bundled with Sabiá (it is a patch on the hostapd build, not a small binary); install it from the Kali repository or build https://github.com/aircrack-ng/hostapd-wpe',
'{n} está num modo que o hostapd-wpe não consegue usar':
'{n} is in a mode hostapd-wpe cannot use',
'não foi possível gerar o certificado TLS (openssl) pro hostapd-wpe':
'could not generate the TLS certificate (openssl) for hostapd-wpe',
'hostapd-wpe saiu sozinho ({n}) logo ao subir em {n} — a placa pode não suportar de verdade o canal/banda do AP real. Saída: {n}':
'hostapd-wpe exited on its own ({n}) right after starting on {n} — the adapter may not actually support the real AP\'s channel/band. Output: {n}',
'job de Evil Twin Enterprise não encontrado': 'Enterprise Evil Twin job not found',
'job de Evil Twin não encontrado': 'Evil Twin job not found',
'hostapd saiu sozinho ({n}) logo ao subir em {n} — a placa pode não suportar de verdade o canal/banda do AP real. Saída: {n}':
'hostapd exited on its own ({n}) right after starting on {n} — the adapter may not actually support the real AP\'s channel/band. Output: {n}',
'{n} está num modo que o hostapd não consegue usar': '{n} is in a mode hostapd cannot use',
'{n} handshake(s), {n} quadro(s) em {n}': '{n} handshake(s), {n} frame(s) in {n}',
'Motivo': 'Reason',
'Pacotes': 'Packets',
'Estado': 'State',
'Probes': 'Probes',
'Cliente': 'Client',
'WPS ativo': 'WPS active',
'WPS ativo.': 'WPS active.',
'WPS ativo ({n}).': 'WPS active ({n}).',
'PMF exigido.': 'PMF required.',
'Depois de coletar IVs suficientes:': 'After collecting enough IVs:',
'mensagens {n}': 'messages {n}',
'{n} · mensagens {n}': '{n} · messages {n}',
'senha: {n}': 'password: {n}',
'{n} · quebrável': '{n} · crackable',
'{n} · canal {n}': '{n} · channel {n}',
'{n} ({n})': '{n} ({n})',
// channels, PMKID/PMF, WPS, and report
'Ocupação dos canais em {n}': 'Channel occupancy on {n}',
'Sem dados de canal ainda': 'No channel data yet',
'Inicie a captura para o Sabiá medir ocupação e sugerir canais.':
'Start capturing so Sabiá can measure occupancy and suggest channels.',
'{n} APs · {n} canais ocupados': '{n} APs · {n} busy channels',
'{n} AP(s) · recomendado': '{n} AP(s) · recommended',
'· recomendado': '· recommended',
'aberta/WEP': 'open/WEP',
'{n} rede(s) WPA3 analisada(s) — detalhes na aba do alvo e no relatório.':
'{n} WPA3 network(s) analyzed — details in the target tab and report.',
'Restrito a 1, 6 e 11 para evitar sobreposição.':
'Limited to 1, 6 and 11 to avoid overlap.',
'Menor pontuação de interferência entre os canais observados.':
'Lowest interference score among observed channels.',
'Melhor canal': 'Best channel',
'Espectro e canais': 'Spectrum and channels',
// spectrum health (channels, SMART style)
'Boa': 'Good',
'Atenção': 'Attention',
'Saturado': 'Saturated',
'Boa condição': 'Good condition',
'Sem canal livre nessa banda': 'No free channel on this band',
'Até o melhor canal está saturado': 'Even the best channel is saturated',
'Recomendado é DFS — pode cair com radar por perto': 'The recommended channel is DFS — it can drop off if radar is detected nearby',
'Espectro congestionado': 'Congested spectrum',
'Canal recomendado': 'Recommended channel',
'Canais ocupados': 'Busy channels',
'APs na banda': 'APs on this band',
'Pior canal': 'Worst channel',
'Canais DFS': 'DFS channels',
'Nessa banda, prefira sempre 1, 6 ou 11 — canais no meio sobrepõem vizinhos.':
'On this band, always prefer 1, 6 or 11 — channels in between overlap neighbors.',
'Canais DFS liberam mais espaço, mas o rádio é obrigado a sair do ar se detectar radar — tenha um canal não-DFS de reserva.':
'DFS channels free up more room, but the radio must go off the air if it detects radar — keep a non-DFS channel as backup.',
'Considere mudar o AP para o canal recomendado ou mover pra outra banda.':
'Consider moving the AP to the recommended channel or to another band.',
'Canais detalhados ({n})': 'Detailed channels ({n})',
'Pontuação': 'Score',
'APs': 'APs',
'{n} AP(s)': '{n} AP(s)',
'Recomendado': 'Recommended',
'Ainda sem eventos além do primeiro avistamento.':
'No events beyond the first sighting yet.',
'{n} amostra(s) de sinal. Eventos mais recentes primeiro.':
'{n} signal sample(s). Most recent events first.',
'nenhuma ferramenta instalada': 'no tool installed',
'Senha encontrada: {n}{n}': 'Password found: {n}{n}',
'Sem PIN, o reaver usa Pixie Dust; o bully usa o ataque padrão.':
'Without a PIN, reaver uses Pixie Dust; bully uses its default attack.',
'Instale reaver ou bully para habilitar. Ex.: sudo apt install reaver bully':
'Install reaver or bully to enable. E.g.: sudo apt install reaver bully',
'A saída do reaver/bully aparece aqui.': 'reaver/bully output appears here.',
'Orquestra reaver/bully na interface monitor. O canal do AP é usado automaticamente; o processo pode ser interrompido a qualquer momento.':
'Orchestrates reaver/bully on the monitor interface. The AP channel is used automatically; the process can be stopped at any time.',
'Ferramenta': 'Tool',
'PIN conhecido (opcional)': 'Known PIN (optional)',
'Exportar .hc22000': 'Export .hc22000',
'Todos .hc22000': 'Export all .hc22000',
'Iniciar WPS': 'Start WPS',
'Parar job': 'Stop job',
'8 dígitos': '8 digits',
'nenhum cliente visto': 'no client seen',
'Direção': 'Direction',
'AP → cliente': 'AP → client',
'cliente → AP': 'client → AP',
'Dispara deauth nas duas direções e observa se a estação reassocia. Resultado prático, não prova criptográfica.':
'Sends deauth in both directions and watches whether the station reassociates. Practical result, not cryptographic proof.',
'Estação': 'Station',
'Forja um MAC local e envia association request com PMKID para tentar a mensagem 1. Muitos APs só expõem PMKID em roaming real.':
'Forge a local MAC and send an association request with PMKID to try for message 1. Many APs only expose PMKID during real roaming.',
'Caçar PMKID': 'Hunt PMKID',
'Do cliente para o AP (exige estação selecionada).':
'From client to AP (requires a selected station).',
'Nas duas direções (exige estação selecionada).':
'In both directions (requires a selected station).',
'{n} quadro(s) {n} em {n} para {n} (motivo {n}).':
'{n} {n} frame(s) in {n} to {n} (reason {n}).',
'Aguardando saída do reaver/bully…': 'Waiting for reaver/bully output…',
'Enviando associação com PMKID…': 'Sending association with PMKID…',
'{n} quadro(s) enviados; PMKID não apareceu. {n}':
'{n} frame(s) sent; PMKID did not appear. {n}',
'Nenhuma estação disponível.': 'No station available.',
'Testando por ~4 s…': 'Testing for ~4 s…',
'PMKID capturado: {n}': 'PMKID captured: {n}',
'PMF não obrigatório': 'PMF not required',
'PMF não obrigatório · {n}': 'PMF not required · {n}',
'Inconclusivo · {n}': 'Inconclusive · {n}',
'Instale reaver ou bully.': 'Install reaver or bully.',
'Canal do AP desconhecido.': 'Unknown AP channel.',
'Montando o relatório…': 'Building the report…',
'não foi possível gerar o relatório': 'could not generate the report',
'Pré-visualização A4 · {n} página(s).': 'A4 preview · {n} page(s).',
'Pré-visualização A4.': 'A4 preview.',
'Paginando o relatório…': 'Paginating the report…',
'Não foi possível abrir a impressão; baixe o HTML e abra no navegador.':
'Could not open printing; download the HTML and open it in the browser.',
'não foi possível gerar o HTML': 'could not generate the HTML',
'Relatório': 'Report',
'Relatório de auditoria': 'Audit report',
'Imprimir / PDF': 'Print / PDF',
'Baixar HTML': 'Download HTML',
'Pré-visualização paginada em A4.': 'Paginated A4 preview.',
'Visualização': 'View',
'Relatório paginado': 'Paginated report',
'handshake sem SSID, PMKID ou mensagem 2 para exportar':
'handshake without SSID, PMKID or message 2 to export',
'nenhum handshake com material para .hc22000':
'no handshake with material for .hc22000',
'direção inválida: use ap, sta ou both': 'invalid direction: use ap, sta or both',
'máscara grande demais pra calcular o total': 'mask too large to calculate the total',
'máscara tem {n} combinações, acima do limite de {n} — restrinja a máscara (ex.: menos posições ?a/?l) antes de rodar':
'mask has {n} combinations, above the limit of {n} — narrow the mask (e.g. fewer ?a/?l positions) before running',
'quadro inválido: use deauth ou disassoc': 'invalid frame: use deauth or disassoc',
'direção sta/both exige a estação alvo': 'sta/both direction requires the target station',
'inicie a captura nesta interface antes do teste PMKID':
'start capturing on this interface before the PMKID test',
'BSSID ainda não visto nesta captura': 'BSSID not seen in this capture yet',
'SSID oculto: impossível montar o association request':
'hidden SSID: cannot build the association request',
'PMKID observado; exporte em .hc22000 e quebre com hashcat ou pelo Sabiá.':
'PMKID observed; export to .hc22000 and crack with hashcat or Sabiá.',
'Nenhum PMKID apareceu. O AP pode não usar cache de PMKID ou só responder a roaming real; tente também desautenticar um cliente associado.':
'No PMKID appeared. The AP may not cache PMKID or may only answer real roaming; also try deauthenticating an associated client.',
'PIN WPS deve ter 8 dígitos': 'WPS PIN must have 8 digits',
'a ferramenta terminou sem informar PSK': 'the tool finished without reporting a PSK',
'job WPS não encontrado': 'WPS job not found',
'Auditoria Wi-Fi · {n}': 'Wi-Fi audit · {n}',
'{n} está em modo managed; ative o modo monitor antes do teste PMKID':
'{n} is in managed mode; enable monitor mode before the PMKID test',
'{n} não encontrado no PATH; instale reaver ou bully':
'{n} not found in PATH; install reaver or bully',
'{n} está em modo managed; ative o modo monitor antes do ataque WPS':
'{n} is in managed mode; enable monitor mode before the WPS attack',
'não foi possível executar {n}: {n}': 'could not run {n}: {n}',
'{n} terminou com código {n}': '{n} exited with code {n}',
// WIDS findings (analysis.rs)
'SSID “{n}” com seguranças diferentes': 'SSID “{n}” with different security levels',
'O mesmo nome aparece em BSSIDs com níveis de segurança distintos: {n}. Pode ser um evil twin ou um AP mal configurado.':
'The same name appears on BSSIDs with different security levels: {n}. It may be an evil twin or a misconfigured AP.',
'Rajada de desautenticação': 'Deauthentication burst',
'{n} quadros de desautenticação/desassociação no último minuto. Pode ser ataque de desautenticação ou um AP instável.':
'{n} deauthentication/disassociation frames in the last minute. It may be a deauth attack or an unstable AP.',
'Muitos BSSIDs novos': 'Many new BSSIDs',
'{n} BSSIDs apareceram no último minuto — possível beacon flood ou ambiente muito denso.':
'{n} BSSIDs appeared in the last minute — possible beacon flood or a very dense environment.',
'Rede aberta: {n}': 'Open network: {n}',
'Sem cifra no Wi-Fi; o tráfego fica exposto a qualquer estação no alcance.':
'No Wi-Fi encryption; traffic is exposed to any station in range.',
'WEP em {n}': 'WEP on {n}',
'WEP é quebrável em minutos; trate a rede como aberta.':
'WEP can be cracked in minutes; treat the network as open.',
'WPA1 (TKIP) em {n}': 'WPA1 (TKIP) on {n}',
'WPA1/TKIP tem fraquezas conhecidas; migre para WPA2/CCMP ou WPA3.':
'WPA1/TKIP has known weaknesses; migrate to WPA2/CCMP or WPA3.',
'WPS ativo em {n}': 'WPS active on {n}',
'O AP anuncia WPS com configuração travada.': 'The AP advertises WPS with configuration locked.',
'WPS sem lock explícito; PIN pode ser testado com reaver/bully.':
'WPS without an explicit lock; the PIN can be tested with reaver/bully.',
'Sem PMF em {n}': 'No PMF on {n}',
'Desautenticação e desassociação não são protegidas; clientes podem ser expulsos para facilitar a captura de handshake.':
'Deauthentication and disassociation are unprotected; clients can be expelled to make handshake capture easier.',
'PMF inconsistente em {n}': 'Inconsistent PMF on {n}',
'O AP anuncia WPA3, mas não anuncia PMF; verifique a configuração.':
'The AP advertises WPA3 but not PMF; check the configuration.',
'WPA3 com WPS em {n}': 'WPA3 with WPS on {n}',
'WPS não faz parte do WPA3 e abre uma via mais fraca de associação.':
'WPS is not part of WPA3 and opens a weaker association path.',
'primeiro visto': 'first seen',
'associou': 'associated',
'desautenticado': 'deauthenticated',
'A estação voltou a se associar.': 'The station reassociated.',
'O AP pode não usar cache de PMKID.': 'The AP may not cache PMKID.',
'A estação voltou a se associar após a desautenticação: PMF não está obrigatório (ou o cliente não o usa).':
'The station reassociated after deauthentication: PMF is not required (or the client does not use it).',
'A estação continuou trocando quadros depois da desautenticação; o quadro de gerenciamento provavelmente foi aceito.':
'The station kept exchanging frames after deauthentication; the management frame was probably accepted.',
'A estação não voltou a associar na janela observada. PMF pode estar ativo, ou o cliente simplesmente ficou inativo.':
'The station did not reassociate in the observed window. PMF may be active, or the client simply stayed idle.',
'Inconclusivo': 'Inconclusive',
// continuous deauth and client identification
'Mantém os quadros saindo em série até você parar (ou até sair o handshake). O canal da captura fica travado no AP durante o job.':
'Keeps frames going until you stop (or a handshake appears). The capture channel stays locked to the AP during the job.',
'Iniciar contínuo': 'Start continuous',
'Parar': 'Stop',
'Rajada ({n})': 'Burst ({n})',
'Pronto. O job contínuo trava a captura no canal do AP e para quando você mandar.':
'Ready. The continuous job locks capture to the AP channel and stops when you say so.',
'Job {n} · {n} quadro(s){n}{n}': 'Job {n} · {n} frame(s){n}{n}',
'Handshake capturado em {n} quadro(s){n}.': 'Handshake captured in {n} frame(s){n}.',
'Job {n} {n} · {n} quadro(s){n}.': 'Job {n} {n} · {n} frame(s){n}.',
'Escolha um cliente para desautenticar nas direções sta/both.':
'Choose a client to deauthenticate in the sta/both directions.',
'Forçando reassociação e handshake…': 'Forcing reassociation and handshake…',
'Desautenticação contínua iniciada.': 'Continuous deauthentication started.',
'Nenhuma ferramenta WPS disponível.': 'No WPS tool available.',
'O reaver embutido usa Pixie Dust sem PIN. O bully aparece se estiver instalado no sistema.':
'The bundled reaver uses Pixie Dust without a PIN. bully appears if it is installed on the system.',
'O reaver embutido não pôde ser extraído nesta execução.':
'The bundled reaver could not be extracted in this run.',
'Dicionário (vazio = embutido, 9,7 milhões de senhas)': 'Wordlist (empty = bundled, 9.7 million passwords)',
'dicionário próprio, opcional': 'custom wordlist, optional',
'Aplicar regras de mutação (maiúscula, sufixos, leet — multiplica o tempo)':
'Apply mutation rules (uppercase, suffixes, leetspeak — multiplies run time)',
'Dispositivo': 'Device',
'MAC aleatório': 'Random MAC',
'Cliente {n}': 'Client {n}',
'Linha do tempo · {n}': 'Timeline · {n}',
'nenhuma ferramenta disponível': 'no tool available',
'· canal {n}': '· channel {n}',
'· para sozinho no handshake': '· stops on handshake',
'job de deauth não encontrado': 'deauth job not found',
'já existe um job de desautenticação em execução em {n}':
'a deauthentication job is already running on {n}',
'inicie a captura em {n} antes de iniciar o job':
'start capture on {n} before starting the job',
'a captura em {n} não está ativa': 'capture on {n} is not active',
'{n} não está disponível; o reaver integrado não pôde ser extraído':
'{n} is not available; the bundled reaver could not be extracted',
};
+8 -8
View File
@@ -1,7 +1,7 @@
// 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").
// UI language. Source strings are in Portuguese; in English mode a translator
// replaces displayed strings (text nodes and attributes) using a dictionary, and
// keeps observing the page to translate dynamically rendered elements.
// Variable placeholders use {n} in the dictionary (e.g. "{n} networks").
import { EN } from './i18n-en.js';
@@ -16,7 +16,7 @@ export function resolveLang(pref) {
}
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
// Dados do usuário (SSID, BSSID, MAC, valores mono) nunca são traduzidos.
// User data (SSID, BSSID, MAC, monospace values) is never translated.
const SKIP = '.ssid, .bssid, .mono, pre, code, input, textarea, script, style';
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
@@ -26,7 +26,7 @@ const patterns = Object.entries(EN)
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
// more specific patterns (more fixed text) first
.sort((a, b) => b.re.source.length - a.re.source.length);
const cache = new Map();
@@ -43,7 +43,7 @@ export function tr(text) {
const m = core.match(p.re);
if (m) {
let i = 0;
// os valores capturados também podem ter partes traduzíveis (ex.: estados)
// captured values may also contain translatable parts (e.g. status labels)
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
break;
}
@@ -55,7 +55,7 @@ export function tr(text) {
return lead + out + trail;
}
const orig = new WeakMap(); // nó -> texto original em português
const orig = new WeakMap(); // node -> original source text
let observer = null;
function translateNode(n) {
+103 -14
View File
@@ -4,31 +4,53 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sabiá</title>
<link rel="icon" href="/sabia.png">
<link rel="icon" href="/logo.png">
<link rel="stylesheet" href="/app.css">
</head>
<body>
<div class="app">
<aside class="sidebar">
<div class="brand">
<img class="logo" src="/sabia.png" alt="">
<img class="logo" src="/logo.png" alt="">
<div>
<strong>Sabiá</strong>
<small>auditoria de Wi-Fi</small>
</div>
</div>
<div class="side-title">Interfaces</div>
<ul class="list" id="ifaceList" data-empty="nenhuma interface Wi-Fi encontrada"></ul>
<div class="side-title">Interface</div>
<div class="iface-pick">
<select id="ifaceSelect" class="select" aria-label="Interface Wi-Fi" data-empty="nenhuma interface Wi-Fi encontrada"></select>
<span id="modePill" class="pill"><span class="dot"></span><span id="modeLabel">—</span></span>
</div>
<div class="side-title">Visualização</div>
<ul class="list" id="viewNav" role="group" aria-label="Visualização">
<li><button type="button" class="dev" data-view="nets" aria-pressed="true">
<span class="ico"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1"/></svg></span>
<span><span class="t">Redes</span><span class="s">APs, clientes e ataques</span></span>
</button></li>
<li><button type="button" class="dev" data-view="channels" aria-pressed="false">
<span class="ico"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="19" x2="5" y2="10"/><line x1="12" y1="19" x2="12" y2="5"/><line x1="19" y1="19" x2="19" y2="13"/></svg></span>
<span><span class="t">Canais</span><span class="s">espectro e saúde</span></span>
</button></li>
</ul>
<div class="side-foot">
<span id="verInfo" class="mono">—</span>
<select id="langSel" class="lang-sel" aria-label="Idioma" title="Idioma">
<option value="auto">Auto</option>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
<button id="btnUpdate" type="button">checar atualização</button>
<div class="foot-row">
<button id="settingsBtn" class="icon-btn" type="button" title="Configurações" aria-label="Configurações">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
</button>
<div class="theme-switch" role="group" aria-label="Tema">
<button type="button" data-theme-pick="auto" aria-pressed="true">Auto</button>
<button type="button" data-theme-pick="light" aria-pressed="false">Claro</button>
<button type="button" data-theme-pick="dark" aria-pressed="false">Escuro</button>
</div>
</div>
<div class="foot-row">
<span id="verInfo" class="mono">—</span>
<button id="btnUpdate" type="button">checar atualização</button>
</div>
</div>
</aside>
@@ -39,7 +61,9 @@
<h1>Painel de captura</h1>
<div class="sub" id="ifaceSub">selecione uma interface</div>
</div>
<span id="modePill" class="pill"><span class="dot"></span><span id="modeLabel">—</span></span>
<div class="top-actions">
<button id="btnReport" class="btn" type="button" disabled>Relatório</button>
</div>
</div>
<svg class="wave" id="wave" viewBox="0 0 600 34" preserveAspectRatio="none">
@@ -68,14 +92,79 @@
</div>
<div class="result-head">
<strong>Redes vistas</strong>
<span id="count">0</span>
<div class="result-summary">
<strong id="resultTitle">Redes vistas</strong>
<span id="count">0</span>
</div>
<div class="result-tools">
<input id="searchInput" class="filter" type="search" placeholder="filtrar por SSID ou BSSID" aria-label="Filtrar redes" autocomplete="off" spellcheck="false">
<select id="sortSel" class="select compact" aria-label="Ordenar por">
<option value="signal">Sinal</option>
<option value="security">Segurança</option>
<option value="clients">Clientes</option>
<option value="name">Nome</option>
</select>
</div>
</div>
<div id="resultArea"></div>
</div>
</main>
</div>
<dialog id="targetDlg" class="target-dlg" aria-labelledby="targetTitle">
<div class="target-head">
<div class="target-id">
<h2 id="targetTitle">—</h2>
<div class="target-sub" id="targetSub"></div>
</div>
<button id="targetClose" class="icon-btn" type="button" aria-label="Fechar" title="Fechar">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</div>
<div class="tabs" role="tablist" aria-label="Detalhes do alvo">
<button type="button" role="tab" data-tab="overview" aria-selected="true">Visão geral</button>
<button type="button" role="tab" data-tab="clients" aria-selected="false">Clientes <span class="tab-count" id="tabClientsCount">0</span></button>
<button type="button" role="tab" data-tab="attacks" aria-selected="false">Ataques</button>
</div>
<div class="tab-panel" id="tabPanel"></div>
</dialog>
<dialog id="reportDlg" class="report-dlg" aria-labelledby="reportTitle">
<div class="report-head">
<div>
<h2 id="reportTitle">Relatório de auditoria</h2>
<p class="hint" id="reportStatus">Pré-visualização paginada em A4.</p>
</div>
<div class="report-tools">
<button type="button" class="icon-btn" id="reportZoomOut" title="Diminuir zoom" aria-label="Diminuir zoom">−</button>
<span class="mono" id="reportZoomLabel">100%</span>
<button type="button" class="icon-btn" id="reportZoomIn" title="Aumentar zoom" aria-label="Aumentar zoom">+</button>
<button type="button" class="btn" id="reportPrint">Imprimir / PDF</button>
<button type="button" class="btn" id="reportDownload">Baixar HTML</button>
<button type="button" class="icon-btn" id="reportClose" title="Fechar" aria-label="Fechar">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
</button>
</div>
</div>
<div class="report-view" id="reportView"><div class="report-canvas" id="reportCanvas"><iframe id="reportFrame" title="Relatório paginado"></iframe></div></div>
</dialog>
<dialog id="settingsDlg" aria-labelledby="settingsTitle">
<form method="dialog">
<h2 id="settingsTitle">Configurações</h2>
<div class="field">
<span class="field-label">Idioma</span>
<select id="langSel" class="select" name="lang" aria-label="Idioma">
<option value="auto">Auto</option>
<option value="pt">Português</option>
<option value="en">English</option>
</select>
</div>
<menu><button class="btn primary" value="close">Fechar</button></menu>
</form>
</dialog>
<script type="module" src="/app.js"></script>
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017-2024 Paged.js contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+10
View File
@@ -0,0 +1,10 @@
# Paged.js
`paged.polyfill.min.js` is the [Paged.js](https://pagedjs.org/) polyfill, version
0.4.3, used to paginate A4 reports in the browser.
- Project: https://gitlab.coko.foundation/pagedjs/pagedjs
- License: MIT (text in `PAGEDJS-LICENSE`)
- File: `paged.polyfill.min.js` (unmodified distribution build)
Vendorized so report generation works completely offline without external CDNs.
+4
View File
@@ -0,0 +1,4 @@
/**
* @license Paged.js v0.4.3 | MIT | https://gitlab.coko.foundation/pagedjs/pagedjs
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).PagedPolyfill=t()}(this,(function(){"use strict";function e(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var t,n,r={exports:{}},a={exports:{}};var i,o,s,l,d=function(){},p=d(),c=function(e){return e!==p&&null!==e};function u(){return l||(l=1,s=(n?t:(n=1,t=function(){try{return Object.keys("primitive"),!0}catch(e){return!1}}))()?Object.keys:function(){if(o)return i;o=1;var e=c,t=Object.keys;return i=function(n){return t(e(n)?Object(n):n)}}()),s}var m,h,g=c,f=function(e){if(!g(e))throw new TypeError("Cannot use null or undefined");return e};var y,b,S=function(){var e,t=Object.assign;return"function"==typeof t&&(t(e={foo:"raz"},{bar:"dwa"},{trzy:"trzy"}),e.foo+e.bar+e.trzy==="razdwatrzy")}()?Object.assign:function(){if(h)return m;h=1;var e=u(),t=f,n=Math.max;return m=function(r,a){var i,o,s,l=n(arguments.length,2);for(r=Object(t(r)),s=function(e){try{r[e]=a[e]}catch(e){i||(i=e)}},o=1;o<l;++o)e(a=arguments[o]).forEach(s);if(void 0!==i)throw i;return r},m}(),v=c,x=Array.prototype.forEach,k=Object.create,w="razdwatrzy";var C,T="function"==typeof w.contains&&!0===w.contains("dwa")&&!1===w.contains("foo")?String.prototype.contains:function(){if(b)return y;b=1;var e=String.prototype.indexOf;return y=function(t){return e.call(this,t,arguments[1])>-1},y}(),_=S,z=function(e){var t=k(null);return x.call(arguments,(function(e){v(e)&&function(e,t){var n;for(n in e)t[n]=e[n]}(Object(e),t)})),t},O=function(e){return"function"==typeof e},A=T;C=a.exports=function(e,t){var n,r,a,i,o;return arguments.length<2||"string"!=typeof e?(i=t,t=e,e=null):i=arguments[2],null==e?(n=a=!0,r=!1):(n=A.call(e,"c"),r=A.call(e,"e"),a=A.call(e,"w")),o={value:t,configurable:n,enumerable:r,writable:a},i?_(z(i),o):o},C.gs=function(e,t,n){var r,a,i,o;return"string"!=typeof e?(i=n,n=t,t=e,e=null):i=arguments[3],null==t?t=void 0:O(t)?null==n?n=void 0:O(n)||(i=n,n=void 0):(i=t,t=n=void 0),null==e?(r=!0,a=!1):(r=A.call(e,"c"),a=A.call(e,"e")),o={get:t,set:n,configurable:r,enumerable:a},i?_(z(i),o):o};var E=a.exports,j=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e};!function(e,t){var n,r,a,i,o,s,l,d=E,p=j,c=Function.prototype.apply,u=Function.prototype.call,m=Object.create,h=Object.defineProperty,g=Object.defineProperties,f=Object.prototype.hasOwnProperty,y={configurable:!0,enumerable:!1,writable:!0};n=function(e,t){var n;return p(t),f.call(this,"__ee__")?n=this.__ee__:(n=y.value=m(null),h(this,"__ee__",y),y.value=null),n[e]?"object"==typeof n[e]?n[e].push(t):n[e]=[n[e],t]:n[e]=t,this},r=function(e,t){var r,i;return p(t),i=this,n.call(this,e,r=function(){a.call(i,e,r),c.call(t,this,arguments)}),r.__eeOnceListener__=t,this},a=function(e,t){var n,r,a,i;if(p(t),!f.call(this,"__ee__"))return this;if(!(n=this.__ee__)[e])return this;if("object"==typeof(r=n[e]))for(i=0;a=r[i];++i)a!==t&&a.__eeOnceListener__!==t||(2===r.length?n[e]=r[i?0:1]:r.splice(i,1));else r!==t&&r.__eeOnceListener__!==t||delete n[e];return this},i=function(e){var t,n,r,a,i;if(f.call(this,"__ee__")&&(a=this.__ee__[e]))if("object"==typeof a){for(n=arguments.length,i=new Array(n-1),t=1;t<n;++t)i[t-1]=arguments[t];for(a=a.slice(),t=0;r=a[t];++t)c.call(r,this,i)}else switch(arguments.length){case 1:u.call(a,this);break;case 2:u.call(a,this,arguments[1]);break;case 3:u.call(a,this,arguments[1],arguments[2]);break;default:for(n=arguments.length,i=new Array(n-1),t=1;t<n;++t)i[t-1]=arguments[t];c.call(a,this,i)}},o={on:n,once:r,off:a,emit:i},s={on:d(n),once:d(r),off:d(a),emit:d(i)},l=g({},s),e.exports=t=function(e){return null==e?m(l):g(Object(e),s)},t.methods=o}(r,r.exports);var W=r.exports,L=e(W);class B{constructor(e){this.context=e||this,this.hooks=[]}register(){for(var e=0;e<arguments.length;++e)if("function"==typeof arguments[e])this.hooks.push(arguments[e]);else for(var t=0;t<arguments[e].length;++t)this.hooks.push(arguments[e][t])}trigger(){var e=arguments,t=this.context,n=[];return this.hooks.forEach((function(r){var a=r.apply(t,e);a&&"function"==typeof a.then?n.push(a):n.push(new Promise(((e,t)=>{e(a)})))})),Promise.all(n)}triggerSync(){var e=arguments,t=this.context,n=[];return this.hooks.forEach((function(r){var a=r.apply(t,e);n.push(a)})),n}list(){return this.hooks}clear(){return this.hooks=[]}}function P(e){if(!e)return;let t;if(void 0!==e.getBoundingClientRect)t=e.getBoundingClientRect();else{let n=document.createRange();n.selectNode(e),t=n.getBoundingClientRect()}return t}function q(e){if(!e)return;let t;if(void 0!==e.getClientRects)t=e.getClientRects();else{let n=document.createRange();n.selectNode(e),t=n.getClientRects()}return t}function M(){var e=(new Date).getTime();return"undefined"!=typeof performance&&"function"==typeof performance.now&&(e+=performance.now()),"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replaceLine truncated
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sabia-cli"
description = "sabia: CLI de auditoria e monitoramento de redes Wi-Fi"
description = "Sabiá: Wi-Fi network auditing and monitoring CLI"
version.workspace = true
edition.workspace = true
license.workspace = true
+32 -26
View File
@@ -16,7 +16,7 @@ use std::time::Duration;
version
)]
struct Cli {
/// Trilha de auditoria (padrão: .sabia-audit.log).
/// Audit log file path (default: `.sabia-audit.log`).
#[arg(long, global = true, default_value = ".sabia-audit.log")]
audit: PathBuf,
@@ -26,91 +26,95 @@ struct Cli {
#[derive(Subcommand)]
enum Cmd {
/// Lista interfaces, canais e controla o modo monitor.
/// List interfaces, channels, and toggle monitor mode.
Iface {
#[command(subcommand)]
action: IfaceAction,
},
/// Captura o ar e mostra redes, clientes e handshakes.
/// Capture live traffic and display networks, clients, and handshakes.
Scan {
iface: String,
/// Canal fixo; sem isso, percorre os canais suportados.
/// Fixed channel; when omitted, hops across supported channels.
#[arg(long)]
channel: Option<u32>,
/// Banda ao percorrer canais: 2.4, 5, 6 ou all.
/// Frequency band when hopping channels: `2.4`, `5`, `6`, or `all`.
#[arg(long)]
band: Option<String>,
/// Tempo em cada canal (ms).
/// Channel dwell time in milliseconds.
#[arg(long, default_value_t = 350)]
hop_ms: u64,
/// Para a varredura depois de N segundos (padrão: até Ctrl+C).
/// Stop scanning after N seconds (default: run until Ctrl+C).
#[arg(long)]
seconds: Option<u64>,
/// Salva os quadros capturados em um .pcap.
/// Save captured frames to a `.pcap` file.
#[arg(long)]
out: Option<PathBuf>,
/// Depois de capturar, tenta quebrar o primeiro handshake.
/// After capturing, attempt a wordlist crack on the first handshake.
#[arg(long, value_name = "DICIONARIO")]
crack: Option<PathBuf>,
/// Mostra apenas o JSON final.
/// Output only the final JSON summary.
#[arg(long)]
json: bool,
},
/// Analisa um .pcap offline (redes, clientes e handshakes).
/// Analyze a `.pcap` file offline (networks, clients, and handshakes).
Analyze {
pcap: PathBuf,
#[arg(long)]
json: bool,
},
/// Quebra um handshake de um .pcap por dicionário.
/// Run a dictionary attack against a handshake in a `.pcap` file.
Crack {
pcap: PathBuf,
/// Dicionário com uma senha por linha.
/// Wordlist file (one candidate passphrase per line).
#[arg(short, long)]
wordlist: PathBuf,
#[arg(long)]
bssid: Option<String>,
#[arg(long)]
station: Option<String>,
/// Apply common mutation rules to each word (capitalization, numeric
/// suffixes, simple leetspeak) — multiplies total runtime.
#[arg(long)]
rules: bool,
#[arg(long)]
json: bool,
},
/// Envia desautenticação para um AP/estação (teste autorizado).
/// Send deauthentication frames to an AP/station (authorized testing only).
Deauth {
iface: String,
#[arg(long)]
bssid: String,
/// Sem isso, o destino é broadcast.
/// Target client MAC; defaults to broadcast when omitted.
#[arg(long)]
station: Option<String>,
#[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.
/// Confirm explicit authorization to test this target network.
#[arg(long)]
yes: bool,
},
/// Diagnóstico de privilégios, ferramentas e interfaces.
/// Diagnose privileges, external tools, and wireless interfaces.
Doctor {
#[arg(long)]
json: bool,
},
/// Inspeciona a trilha de auditoria.
/// Inspect and verify the hash-chained audit trail.
Audit {
#[command(subcommand)]
action: AuditAction,
},
/// Verifica e aplica atualizações do próprio sabia.
/// Check for and apply `sabia` CLI updates.
Update {
#[arg(long)]
apply: bool,
},
/// Abre a interface gráfica (servidor local + navegador).
/// Launch the web UI (local HTTP server + browser).
Ui {
#[arg(long, default_value = "4337")]
port: u16,
/// Não tenta abrir o navegador automaticamente.
/// Do not open the browser automatically.
#[arg(long)]
no_open: bool,
},
@@ -119,13 +123,13 @@ enum Cmd {
#[derive(Subcommand)]
enum IfaceAction {
List,
/// Mostra os canais suportados pelo rádio.
/// List channels supported by the wireless phy.
Channels {
iface: String,
#[arg(long)]
json: bool,
},
/// Liga/desliga modo monitor numa interface Wi-Fi.
/// Enable or disable monitor mode on a Wi-Fi interface.
Monitor {
iface: String,
#[arg(value_enum)]
@@ -135,9 +139,9 @@ enum IfaceAction {
#[derive(Subcommand)]
enum AuditAction {
/// Confere a integridade da cadeia.
/// Verify the cryptographic hash chain integrity.
Verify { path: Option<PathBuf> },
/// Mostra as últimas entradas.
/// Show the last N entries.
Show {
#[arg(long, default_value_t = 20)]
limit: usize,
@@ -275,6 +279,7 @@ fn run(cli: Cli) -> sabia_core::Result<()> {
wordlist,
bssid,
station,
rules,
json,
} => {
let mut last = 0u64;
@@ -283,6 +288,7 @@ fn run(cli: Cli) -> sabia_core::Result<()> {
&wordlist,
bssid.as_deref(),
station.as_deref(),
rules,
|tried| {
if tried.saturating_sub(last) >= 4096 {
eprint!("\r{tried} senhas testadas…");
@@ -412,7 +418,7 @@ fn crack_first(engine: &Engine, iface: &str, wordlist: &PathBuf) -> sabia_core::
hs.bssid,
wordlist.display()
);
let id = engine.start_crack(iface, Some(&hs.bssid), Some(&hs.station), wordlist)?;
let id = engine.start_crack(iface, Some(&hs.bssid), Some(&hs.station), wordlist, false)?;
loop {
let job = engine.crack_job(&id).expect("job");
if job.status == "running" {
+357 -10
View File
@@ -1,7 +1,7 @@
//! Servidor local da interface: uma única página HTML/JS servida em
//! `127.0.0.1`, protegida por um token gerado na hora, falando com o
//! 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.
//! Local web UI server: serves the single-page HTML/JS app on
//! `127.0.0.1`, protected by a per-session random token, communicating with
//! the core over JSON endpoints. The exact same frontend in `apps/gui/ui`
//! powers the Tauri desktop app.
use sabia_core::engine::{Engine, ScanOptions};
use serde::Deserialize;
@@ -15,13 +15,29 @@ 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_PNG: &[u8] = include_bytes!("../../../../apps/gui/ui/sabia.png");
const I18N_JS: &str = include_str!("../../../../apps/gui/ui/i18n.js");
const I18N_EN_JS: &str = include_str!("../../../../apps/gui/ui/i18n-en.js");
const LOGO_PNG: &[u8] = include_bytes!("../../../../logo.png");
const PAGED_JS: &str = include_str!("../../../../apps/gui/ui/vendor/paged.polyfill.min.js");
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::with_audit_path(audit_path));
// Without a signal handler, Ctrl+C (or `SIGTERM`) during an active Evil Twin
// exits before cleanup runs: `hostapd`/`dnsmasq` are left orphaned
// and both interfaces remain stuck in `managed no` under
// NetworkManager until the next engine startup runs self-healing.
//
{
let engine = engine.clone();
let _ = ctrlc::set_handler(move || {
engine.shutdown_cleanup();
std::process::exit(0);
});
}
println!("Sabiá UI em http://127.0.0.1:{port}/?token={token}");
if open {
let _ = open_browser(&format!("http://127.0.0.1:{port}/?token={token}"));
@@ -225,7 +241,48 @@ fn handle(
);
}
if req.path == "/api/report.html" {
if req.token.as_deref() != Some(expected_token) {
return respond(&mut stream, 401, "text/plain", b"token invalido");
}
let Some(iface) = req.text("iface") else {
return respond(&mut stream, 400, "text/plain", b"iface obrigatoria");
};
return match engine.report(&iface) {
Ok(model) => {
let download = req.bool("download");
let mut html = sabia_core::report::render(
&model,
sabia_core::report::ReportOptions {
inline_pagedjs: download,
},
);
if download {
html = html.replace(
"<!--SABIA_PAGEDJS-->",
&format!("<script>{PAGED_JS}</script>"),
);
}
respond(
&mut stream,
200,
"text/html; charset=utf-8",
html.as_bytes(),
)
}
Err(e) => respond(&mut stream, 400, "text/plain", e.to_string().as_bytes()),
};
}
match req.path.as_str() {
"/vendor/paged.polyfill.min.js" | "/paged.polyfill.min.js" => {
return respond(
&mut stream,
200,
"application/javascript",
PAGED_JS.as_bytes(),
)
}
"/app.css" => return respond(&mut stream, 200, "text/css", APP_CSS.as_bytes()),
"/app.js" => {
return respond(
@@ -235,7 +292,23 @@ fn handle(
APP_JS.as_bytes(),
)
}
"/sabia.png" => return respond(&mut stream, 200, "image/png", LOGO_PNG),
"/i18n.js" => {
return respond(
&mut stream,
200,
"application/javascript",
I18N_JS.as_bytes(),
)
}
"/i18n-en.js" => {
return respond(
&mut stream,
200,
"application/javascript",
I18N_EN_JS.as_bytes(),
)
}
"/logo.png" => return respond(&mut stream, 200, "image/png", LOGO_PNG),
_ => {}
}
@@ -408,13 +481,210 @@ fn api(engine: &Arc<Engine>, req: &Request) -> (u16, String) {
Err(e) => err(400, e),
}
}
("POST", "/api/handshakes/export-hc22000") => {
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_hc22000(&iface, &bssid, &station, &out) {
Ok(report) => ok(&report),
Err(e) => err(400, e),
}
}
("POST", "/api/handshakes/export-all-hc22000") => {
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_hc22000(&iface, &out) {
Ok(report) => ok(&report),
Err(e) => err(400, e),
}
}
("GET", "/api/report") => {
let Some(iface) = req.text("iface") else {
return err(400, "iface obrigatória");
};
match engine.report(&iface) {
Ok(model) => ok(&model),
Err(e) => err(400, e),
}
}
("GET", "/api/wps/tools") => ok(&engine.wps_tools()),
("POST", "/api/wps") => {
if !req.bool("confirm") {
return err(400, "confirme a autorização do teste (confirm=true)");
}
let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else {
return err(400, "iface e bssid são obrigatórios");
};
let tool = req.text("tool").unwrap_or_else(|| "reaver".into());
let channel = req.u32("channel").unwrap_or(0);
let pin = req.text("pin");
match engine.start_wps(&iface, &bssid, channel, &tool, pin.as_deref()) {
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
Err(e) => err(400, e),
}
}
("GET", "/api/wps") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.wps_job(&id) {
Some(job) => ok(&job),
None => err(404, "job não encontrado"),
}
}
("GET", "/api/wps/jobs") => ok(&engine.wps_jobs()),
("POST", "/api/wps/stop") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.stop_wps(&id) {
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
Err(e) => err(400, e),
}
}
("POST", "/api/eviltwin") => {
if !req.bool("confirm") {
return err(400, "confirme a autorização do teste (confirm=true)");
}
let (Some(iface), Some(ap_iface), Some(bssid)) =
(req.text("iface"), req.text("ap_iface"), req.text("bssid"))
else {
return err(400, "iface, ap_iface e bssid são obrigatórios");
};
let portal_port = req.u16("portal_port").unwrap_or(8080);
let deauth = req.bool("deauth");
match engine.start_evil_twin(&iface, &ap_iface, &bssid, portal_port, deauth) {
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
Err(e) => err(400, e),
}
}
("GET", "/api/eviltwin") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.eviltwin_job(&id) {
Some(job) => ok(&job),
None => err(404, "job não encontrado"),
}
}
("GET", "/api/eviltwins") => ok(&engine.eviltwin_jobs()),
("POST", "/api/eviltwin/stop") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.stop_evil_twin(&id) {
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
Err(e) => err(400, e),
}
}
("POST", "/api/eviltwin-enterprise") => {
if !req.bool("confirm") {
return err(400, "confirme a autorização do teste (confirm=true)");
}
let (Some(iface), Some(ap_iface), Some(bssid)) =
(req.text("iface"), req.text("ap_iface"), req.text("bssid"))
else {
return err(400, "iface, ap_iface e bssid são obrigatórios");
};
match engine.start_evil_twin_enterprise(&iface, &ap_iface, &bssid) {
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
Err(e) => err(400, e),
}
}
("GET", "/api/eviltwin-enterprise") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.enterprise_twin_job(&id) {
Some(job) => ok(&job),
None => err(404, "job não encontrado"),
}
}
("GET", "/api/eviltwins-enterprise") => ok(&engine.enterprise_twin_jobs()),
("POST", "/api/eviltwin-enterprise/stop") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.stop_evil_twin_enterprise(&id) {
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
Err(e) => err(400, e),
}
}
("POST", "/api/pmkid") => {
if !req.bool("confirm") {
return err(400, "confirme a autorização do teste (confirm=true)");
}
let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else {
return err(400, "iface e bssid são obrigatórios");
};
let attempts = req.u32("attempts").unwrap_or(5);
match engine.pmkid_hunt(&iface, &bssid, attempts) {
Ok(report) => ok(&report),
Err(e) => err(400, e),
}
}
("POST", "/api/pmf") => {
if !req.bool("confirm") {
return err(400, "confirme a autorização do teste (confirm=true)");
}
let (Some(iface), Some(bssid), Some(station)) =
(req.text("iface"), req.text("bssid"), req.text("station"))
else {
return err(400, "iface, bssid e station são obrigatórios");
};
let reason = req.u16("reason").unwrap_or(7);
match engine.pmf_test(&iface, &bssid, &station, reason) {
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 Some(iface) = req.text("iface") else {
return err(400, "iface é obrigatória");
};
let bssid = req.text("bssid");
let station = req.text("station");
match engine.start_crack(&iface, bssid.as_deref(), station.as_deref(), &wordlist) {
let rules = req.bool("rules");
let result = match req.text("wordlist") {
Some(wordlist) if !wordlist.trim().is_empty() => engine.start_crack(
&iface,
bssid.as_deref(),
station.as_deref(),
&wordlist,
rules,
),
_ => {
engine.start_crack_default(&iface, bssid.as_deref(), station.as_deref(), rules)
}
};
match result {
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
Err(e) => err(400, e),
}
}
("POST", "/api/crack/mask") => {
let Some(iface) = req.text("iface") else {
return err(400, "iface é obrigatória");
};
let Some(mask) = req.text("mask").filter(|m| !m.trim().is_empty()) else {
return err(400, "máscara é obrigatória");
};
let bssid = req.text("bssid");
let station = req.text("station");
let max_candidates = req.u64("max_candidates").unwrap_or(50_000_000);
match engine.start_crack_mask(
&iface,
bssid.as_deref(),
station.as_deref(),
&mask,
max_candidates,
) {
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
Err(e) => err(400, e),
}
@@ -441,17 +711,94 @@ fn api(engine: &Arc<Engine>, req: &Request) -> (u16, String) {
};
let count = req.u32("count").unwrap_or(5);
let reason = req.u16("reason").unwrap_or(7);
match engine.deauth(
let direction = req.text("direction").unwrap_or_else(|| "ap".into());
let frame_kind = req.text("frame").unwrap_or_else(|| "deauth".into());
match engine.deauth_directional(
&iface,
&bssid,
req.text("station").as_deref(),
count,
reason,
&direction,
&frame_kind,
) {
Ok(report) => ok(&report),
Err(e) => err(400, e),
}
}
("POST", "/api/deauth/start") => {
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 reason = req.u16("reason").unwrap_or(7);
let direction = req.text("direction").unwrap_or_else(|| "both".into());
let frame_kind = req.text("frame").unwrap_or_else(|| "deauth".into());
let interval_ms = req.u64("interval_ms").unwrap_or(120);
let max_secs = req.u64("max_secs");
let stop_on_handshake = req.bool("stop_on_handshake");
match engine.start_deauth_job(
&iface,
&bssid,
req.text("station").as_deref(),
reason,
&direction,
&frame_kind,
interval_ms,
max_secs,
stop_on_handshake,
) {
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
Err(e) => err(400, e),
}
}
("POST", "/api/deauth/stop") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.stop_deauth_job(&id) {
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
Err(e) => err(400, e),
}
}
("GET", "/api/deauth/job") => {
let Some(id) = req.text("id") else {
return err(400, "id obrigatório");
};
match engine.deauth_job(&id) {
Some(job) => ok(&job),
None => err(404, "job não encontrado"),
}
}
("GET", "/api/deauth/jobs") => {
let jobs = engine.deauth_jobs();
match req.text("iface") {
Some(iface) => ok(&jobs
.into_iter()
.filter(|j| j.iface.eq_ignore_ascii_case(&iface))
.collect::<Vec<_>>()),
None => ok(&jobs),
}
}
("POST", "/api/handshakes/capture") => {
if !req.bool("confirm") {
return err(400, "confirme a autorização do teste (confirm=true)");
}
let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else {
return err(400, "iface e bssid são obrigatórios");
};
let seconds = req.u64("seconds");
match engine.capture_handshake(&iface, &bssid, req.text("station").as_deref(), seconds)
{
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
Err(e) => err(400, e),
}
}
("POST", "/api/analyze") => {
let pcap = if !req.body.is_empty() {
serde_json::from_slice::<AnalyzeBody>(&req.body)
+5 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "sabia-core"
description = "Sabiá: núcleo de captura, análise e auditoria de redes Wi-Fi"
description = "Sabiá: Wi-Fi network capture, analysis, and auditing core"
version.workspace = true
edition.workspace = true
license.workspace = true
@@ -18,10 +18,12 @@ sha2 = "0.10"
sha1 = "0.10"
hmac = "0.12"
md-5 = "0.10"
aes = "0.8"
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
[dev-dependencies]
tempfile = "3"
[target.'cfg(not(target_os = "linux"))'.dependencies]
# captura crua (AF_PACKET) só existe no Linux; outras plataformas compilam
# a crate mas os módulos de captura ficam indisponíveis em tempo de execução.
# Raw capture (AF_PACKET) is Linux-only; other platforms compile the crate
# while live capture modules remain unavailable at runtime.
@@ -0,0 +1,100 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: reaver-wps-fork-t6x
Source: https://github.com/t6x/reaver-wps-fork-t6x
Files: *
Copyright: 2011, Tactical Network Solutions, Craig Heffner <cheffner@tacnetsol.com>
License: GPL-2
Files: src/common/*
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
License: GPL-2
Files: src/crypto/*
Copyright: 2002-2010, Jouni Malinen <j@w1.fi> and contributors
License: GPL-2
Files: src/tls/*
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
License: GPL-2
Files: src/tls/libtommath.c
Copyright: Tom St Denis
License: public-domain
Files: src/utils/*
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
License: GPL-2
Files: src/utils/radiotap.h
Copyright: 2003-2004 David Young
License: BSD-3-clause
Files: src/wps/*
Copyright: 2007-2009, Jouni Malinen <j@w1.fi>
License: GPL-2
Files: src/wps/wps_ufd.c
Copyright: 2009, Masashi Honma <honma@ictec.co.jp>
License: GPL-2
Files: src/lwe/*
Copyright: 1997-2007, Jean Tourrilhes <jt@hpl.hp.com>
License: GPL-2
Files: debian/*
Copyright: 2011-2018 Bartosz Fenski <fenio@debian.org>
2023 Leandro Cunha <leandrocunha016@gmail.com>
License: GPL-2
License: GPL-2
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this package; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
.
On Debian systems, the complete text of the GNU General
Public License can be found in `/usr/share/common-licenses/GPL-2'.
License: BSD-3-clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
.
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
.
3. Neither the name of the ORGANIZATION nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
License: public-domain
The files tagged with this license contains the following paragraphs:
.
No copyright is claimed. This code is in the public domain; do with
it what you wish.
.
Written by Karel Zak <kzak@redhat.com>
Binary file not shown.
@@ -0,0 +1,31 @@
wpa-full.txt.gz
Dois dicionários concatenados, com as duplicatas removidas e a ordem de
prioridade preservada (as entradas mais prováveis ficam primeiro, então a
quebra por dicionário tende a achar a senha mais cedo em vez de só no fim):
1. Probable-Wordlists (berzerk0), Real-Passwords/WPA-Length/
Top204Thousand-WPA-probable-v2.txt — 203.806 senhas reais vazadas, já
filtradas pro comprimento válido de WPA (8–63) e ordenadas por frequência
observada no mundo real.
https://github.com/berzerk0/Probable-Wordlists
Licença: Creative Commons Attribution-ShareAlike 4.0 International
(CC BY-SA 4.0) — https://creativecommons.org/licenses/by-sa/4.0/
2. rockyou.txt — o dump de senhas em texto puro do vazamento da RockYou de
2009 (~32 milhões de contas), aqui filtrado pro mesmo comprimento válido
de WPA. É provavelmente o dicionário de senhas mais usado em segurança
ofensiva — vem embutido por padrão até no Kali Linux
(/usr/share/wordlists/rockyou.txt.gz). Redistribuído aqui via SecLists:
https://github.com/danielmiessler/SecLists (repositório MIT; o conteúdo do
próprio rockyou.txt é dado de vazamento, sem titular de direitos autorais
que o licencie formalmente — é tratado como de uso livre pra pesquisa de
segurança em toda a indústria, mesmo padrão que o SecLists e as
distribuições Linux de pentest adotam).
Ao redistribuir este arquivo (ou um derivado dele), mantenha a atribuição ao
Probable-Wordlists acima — é a parte que tem uma licença formal que exige
isso (CC BY-SA 4.0).
Isso é atribuição e aviso de licença, não os termos legais completos da
CC BY-SA 4.0.
Binary file not shown.
Loaded 100 of 160 files, more files were not shown because too many files have changed in this diff. Show more