Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d69f2e4b2 | ||
|
|
ba0c99d5b6 | ||
|
|
476e27e253 | ||
|
|
55e19a8e5b | ||
|
|
bf059fd0bc | ||
|
|
a904723f96 | ||
|
|
2dfbbfc64a | ||
|
|
659701b2d4 | ||
|
|
30a086a808 | ||
|
|
833abc6308 |
No files matched your search
@@ -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 }
|
||||||
@@ -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)
|
||||||
@@ -1,31 +1,32 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
# Mudança só de documentação não precisa de 6 jobs em 3 sistemas.
|
# Documentation-only or local agent state changes do not trigger CI jobs.
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', 'scripts/release-notes.sh', '.gitea/workflows/release.yml']
|
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:
|
pull_request:
|
||||||
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', 'scripts/release-notes.sh', '.gitea/workflows/release.yml']
|
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:
|
concurrency:
|
||||||
group: ci-${{ github.ref }}
|
group: ci-${{ gitea.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUSTFLAGS: -D warnings
|
RUSTFLAGS: -D warnings
|
||||||
# O runner mantém /opt/hostedtoolcache num volume que sobrevive entre jobs:
|
# The runner persists /opt/hostedtoolcache across jobs: rustup toolchains,
|
||||||
# a toolchain do rustup, o registry do cargo e o diretório target ficam ali,
|
# cargo registry, npm/apt caches, and target directories live there so nothing
|
||||||
# então nada disso é baixado ou recompilado do zero a cada execução. Cada job
|
# is re-downloaded or rebuilt from scratch on every run. Each job uses its own
|
||||||
# usa o seu próprio target (flags diferentes invalidariam o de outro job).
|
# target directory so differing RUSTFLAGS do not invalidate another job's cache.
|
||||||
CARGO_HOME: /opt/hostedtoolcache/cargo
|
CARGO_HOME: /opt/hostedtoolcache/cargo
|
||||||
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
||||||
|
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Formatação e clippy
|
name: Format and clippy
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-lint
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-lint
|
||||||
steps:
|
steps:
|
||||||
@@ -36,15 +37,15 @@ jobs:
|
|||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Cobertura da tradução da interface
|
- name: UI translation coverage
|
||||||
run: node scripts/check-ui-i18n.mjs
|
run: node scripts/check-ui-i18n.mjs
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
- run: cargo clippy --workspace --all-targets
|
- run: cargo clippy --workspace --all-targets
|
||||||
|
|
||||||
# Windows e macOS saem: o Gitea aqui tem um único runner, Linux arm64.
|
# Linux-only runner (ARM64).
|
||||||
test:
|
test:
|
||||||
name: Testes (Linux)
|
name: Tests (Linux)
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-test
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-test
|
||||||
steps:
|
steps:
|
||||||
@@ -53,22 +54,22 @@ jobs:
|
|||||||
- run: cargo test --workspace
|
- run: cargo test --workspace
|
||||||
|
|
||||||
audit:
|
audit:
|
||||||
name: Auditoria de dependências
|
name: Dependency audit
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
# rustsec/audit-check depends on GitHub's checks API. cargo-audit itself
|
# rustsec/audit-check depends on GitHub's checks API. cargo-audit itself
|
||||||
# needs nothing but the advisory database, and installing it once into the
|
# needs nothing but the advisory database, and installing it once into the
|
||||||
# persistent CARGO_HOME makes every later run skip the compile.
|
# persistent CARGO_HOME makes every later run skip compilation.
|
||||||
- name: cargo audit
|
- name: cargo audit
|
||||||
run: |
|
run: |
|
||||||
command -v cargo-audit >/dev/null || cargo install cargo-audit --locked
|
command -v cargo-audit >/dev/null || cargo install cargo-audit --locked
|
||||||
cargo audit
|
cargo audit
|
||||||
|
|
||||||
gui:
|
gui:
|
||||||
name: Compila o app desktop (Linux)
|
name: Check desktop app (Linux)
|
||||||
runs-on: ubuntu-latest
|
runs-on: linux-arm64
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-gui
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-gui
|
||||||
steps:
|
steps:
|
||||||
@@ -77,15 +78,10 @@ jobs:
|
|||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Cache dos pacotes do Tauri (.deb)
|
- name: Tauri system dependencies
|
||||||
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
|
timeout-minutes: 25
|
||||||
run: scripts/apt-install.sh ~/apt-cache libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
- name: Gera os ícones
|
- 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: 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
|
- run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,24 +1,28 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
# Quando uma tag v* é enviada: cria a entrada em "Releases" com as notas do
|
# Triggered when a v* tag is pushed: creates the Gitea Release entry with notes
|
||||||
# CHANGELOG.md e anexa o CLI compilado (scripts/package-cli.sh), com um .sha256 ao
|
# from CHANGELOG.md and uploads the compiled CLI (scripts/package-cli.sh) with
|
||||||
# lado de cada pacote. É o que o atualizador (`sabia update`) baixa.
|
# a .sha256 checksum alongside each archive, plus the desktop bundles (Linux
|
||||||
|
# aarch64, Linux x86_64, and Windows via cargo-xwin).
|
||||||
#
|
#
|
||||||
# O CLI é só para Linux (x86_64 e aarch64), compilado no runner Linux; o x86_64
|
# Updater signing (optional): SABIA_TAURI_SIGNING_PRIVATE_KEY and
|
||||||
# por compilação cruzada. Windows e macOS têm só o app desktop.
|
# SABIA_TAURI_SIGNING_PRIVATE_KEY_PASSWORD, with unprefixed names as fallback.
|
||||||
|
# Without a key, the build succeeds without generating .sig files.
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags: ['v*']
|
tags: ['v*']
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
# Toolchain e registry do cargo ficam no volume que sobrevive entre jobs.
|
# Rust toolchains, cargo registry, and caches live on the persistent runner volume.
|
||||||
CARGO_HOME: /opt/hostedtoolcache/cargo
|
CARGO_HOME: /opt/hostedtoolcache/cargo
|
||||||
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
||||||
|
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
|
||||||
|
XWIN_CACHE_DIR: /opt/hostedtoolcache/xwin-cache
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
name: Cria a release
|
name: Create release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
@@ -30,9 +34,9 @@ jobs:
|
|||||||
TOKEN: ${{ gitea.token }}
|
TOKEN: ${{ gitea.token }}
|
||||||
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
run: |
|
run: |
|
||||||
# Uma execução repetida não recria a release.
|
# 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
|
if curl -fsS -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" >/dev/null 2>&1; then
|
||||||
echo "a release $TAG já existe"; exit 0
|
echo "release $TAG already exists"; exit 0
|
||||||
fi
|
fi
|
||||||
scripts/release-notes.sh "$TAG" > notes.md
|
scripts/release-notes.sh "$TAG" > notes.md
|
||||||
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
||||||
@@ -44,62 +48,84 @@ jobs:
|
|||||||
cli:
|
cli:
|
||||||
name: CLI (${{ matrix.platform }})
|
name: CLI (${{ matrix.platform }})
|
||||||
needs: release
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-cli-${{ matrix.platform }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- { platform: linux-aarch64, target: aarch64-unknown-linux-gnu, apt: "" }
|
- { 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" }
|
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu", runner: linux-amd64 }
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
- name: Linker e ferramentas desta plataforma
|
- name: Platform cross-linker
|
||||||
if: ${{ matrix.apt != '' }}
|
if: ${{ matrix.apt != '' && matrix.runner != 'linux-amd64' }}
|
||||||
run: |
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/cli-x86_64 ${{ matrix.apt }}
|
||||||
apt-get update -qq
|
- name: Build and package
|
||||||
apt-get install -y -qq ${{ matrix.apt }}
|
|
||||||
- name: Compila e empacota
|
|
||||||
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
|
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
|
||||||
- name: Anexa à release
|
- name: Upload to release
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ gitea.token }}
|
TOKEN: ${{ gitea.token }}
|
||||||
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
run: scripts/release-upload.sh "${{ gitea.ref_name }}" $(cat packages.txt)
|
run: scripts/release-upload.sh "${{ gitea.ref_name }}" $(cat packages.txt)
|
||||||
|
|
||||||
gui:
|
gui:
|
||||||
name: App desktop (${{ matrix.platform }})
|
name: Desktop app (${{ matrix.platform }})
|
||||||
needs: release
|
needs: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
env:
|
||||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-gui
|
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:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform: [linux, windows]
|
include:
|
||||||
|
- { platform: linux-aarch64, runner: linux-arm64 }
|
||||||
|
- { platform: linux-x86_64, runner: linux-amd64 }
|
||||||
|
- { platform: windows, runner: linux-amd64 }
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Dependências do Tauri (Linux)
|
- name: Tauri dependencies (Linux aarch64)
|
||||||
if: matrix.platform == 'linux'
|
if: matrix.platform == 'linux-aarch64'
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
run: scripts/apt-install.sh ~/apt-cache libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
- name: Ferramentas para compilação cruzada (Windows)
|
- 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'
|
if: matrix.platform == 'windows'
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-win nsis lld llvm clang
|
||||||
apt-get install -y -qq nsis lld llvm clang
|
|
||||||
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
|
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
|
||||||
- name: Compila e empacota
|
- name: Build and package
|
||||||
run: scripts/build-gui.sh "${{ matrix.platform }}"
|
run: scripts/build-gui.sh "${{ matrix.platform }}"
|
||||||
- name: Anexa à release
|
- name: Upload to release
|
||||||
env:
|
env:
|
||||||
TOKEN: ${{ gitea.token }}
|
TOKEN: ${{ gitea.token }}
|
||||||
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
exec "$(git rev-parse --show-toplevel)/scripts/check.sh"
|
||||||
@@ -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 }
|
|
||||||
@@ -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)
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
/target
|
# Build artifacts
|
||||||
|
target/
|
||||||
|
apps/gui/src-tauri/target/
|
||||||
|
node_modules/
|
||||||
|
*.img
|
||||||
|
*.guara-session.json
|
||||||
|
.DS_Store
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# Sabiá local captures and audit log
|
||||||
*.pcap
|
*.pcap
|
||||||
*.cap
|
*.cap
|
||||||
.sabia-audit.log
|
.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
|
# Logo copied to the UI directory by build.rs
|
||||||
# produto, fica só na máquina de desenvolvimento.
|
/apps/gui/ui/logo.png
|
||||||
|
|
||||||
|
# Local AI agent configuration and state — not part of the product
|
||||||
.claude/
|
.claude/
|
||||||
.wolf/
|
.wolf/
|
||||||
.opencode/
|
.opencode/
|
||||||
@@ -19,6 +27,16 @@ CLAUDE.md
|
|||||||
AGENTS.md
|
AGENTS.md
|
||||||
GEMINI.md
|
GEMINI.md
|
||||||
|
|
||||||
# Pacotes gerados por scripts/package-cli.sh
|
# 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*
|
/sabia-v*
|
||||||
/dist-gui
|
/dist-gui
|
||||||
@@ -4,6 +4,15 @@
|
|||||||
|
|
||||||
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
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
|
## [1.0.2] - 2026-10-30
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -4,6 +4,15 @@
|
|||||||
|
|
||||||
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
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
|
## [1.0.2] - 2026-10-30
|
||||||
|
|
||||||
### Corrigido
|
### Corrigido
|
||||||
|
|||||||
@@ -7,29 +7,25 @@ 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.
|
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.
|
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.
|
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
|
## Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo fmt --all
|
scripts/install-hooks.sh # once: enables commit-msg and pre-push Git hooks
|
||||||
cargo clippy --workspace --all-targets # as in CI (RUSTFLAGS=-D warnings)
|
scripts/check.sh # fast (seconds): fmt, clippy as in CI, i18n, scripts, workflows, semver
|
||||||
cargo test --workspace
|
scripts/check.sh --full # adds cargo test --workspace and cargo audit
|
||||||
node scripts/check-ui-i18n.mjs # UI translation coverage
|
|
||||||
```
|
```
|
||||||
|
|
||||||
CI runs on Linux only (Gitea has one runner); 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); the code behind Windows and macOS paths still compiles and is covered by `cargo check`, but neither runs in CI nor is built for release.
|
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
|
## Releases and Versioning
|
||||||
|
|
||||||
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 **aarch64** and Windows. The runner is arm64, so the Linux **x86_64** desktop app comes from an x86_64 Linux machine:
|
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`).
|
||||||
|
|
||||||
```bash
|
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.
|
||||||
scripts/build-gui.sh linux # AppImage + .deb in dist-gui/
|
|
||||||
API=https://git.lucasrguerra.dev.br/api/v1/repos/lucasrguerra/sabia TOKEN=... \
|
|
||||||
scripts/release-upload.sh vX.Y.Z dist-gui/*
|
|
||||||
```
|
|
||||||
|
|
||||||
There is no macOS build: it needs Apple's SDK and code signing, which a Linux runner cannot provide.
|
There is no macOS build: it needs Apple's SDK and code signing, which a Linux runner cannot provide.
|
||||||
|
|
||||||
@@ -50,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.
|
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
|
## License
|
||||||
|
|
||||||
By contributing, you agree to license under [MIT](LICENSE-MIT) **or** [Apache-2.0](LICENSE-APACHE), like the rest of the project.
|
By contributing, you agree to license under [MIT](LICENSE-MIT) **or** [Apache-2.0](LICENSE-APACHE), like the rest of the project.
|
||||||
@@ -7,29 +7,25 @@ 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.
|
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.
|
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.
|
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
|
## Fluxo
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo fmt --all
|
scripts/install-hooks.sh # uma vez: ativa os hooks commit-msg e pre-push
|
||||||
cargo clippy --workspace --all-targets # como no CI (RUSTFLAGS=-D warnings)
|
scripts/check.sh # rápido (segundos): fmt, clippy como no CI, tradução, scripts, workflows, semver
|
||||||
cargo test --workspace
|
scripts/check.sh --full # inclui cargo test --workspace e cargo audit
|
||||||
node scripts/check-ui-i18n.mjs # cobertura da tradução da interface
|
|
||||||
```
|
```
|
||||||
|
|
||||||
O CI roda só em Linux (o Gitea tem um único runner); 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); o código dos caminhos de Windows e macOS continua compilando e é coberto pelo `cargo check`, mas não roda no CI nem é compilado para release. 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
|
## Releases e Versionamento
|
||||||
|
|
||||||
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 **aarch64** e Windows. O runner é arm64, então o app Linux **x86_64** vem de uma máquina Linux x86_64:
|
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`).
|
||||||
|
|
||||||
```bash
|
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.
|
||||||
scripts/build-gui.sh linux # AppImage + .deb em dist-gui/
|
|
||||||
API=https://git.lucasrguerra.dev.br/api/v1/repos/lucasrguerra/sabia TOKEN=... \
|
|
||||||
scripts/release-upload.sh vX.Y.Z dist-gui/*
|
|
||||||
```
|
|
||||||
|
|
||||||
Não há build para macOS: ele exige o SDK e a assinatura da Apple, que um runner Linux não tem.
|
Não há build para macOS: ele exige o SDK e a assinatura da Apple, que um runner Linux não tem.
|
||||||
|
|
||||||
@@ -50,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.
|
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
|
## Licença
|
||||||
|
|
||||||
Ao contribuir, você concorda em licenciar sob [MIT](LICENSE-MIT) **ou** [Apache-2.0](LICENSE-APACHE), como o resto do projeto.
|
Ao contribuir, você concorda em licenciar sob [MIT](LICENSE-MIT) **ou** [Apache-2.0](LICENSE-APACHE), como o resto do projeto.
|
||||||
@@ -440,7 +440,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-cli"
|
name = "sabia-cli"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
@@ -451,7 +451,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-core"
|
name = "sabia-core"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ resolver = "2"
|
|||||||
members = ["crates/sabia-core", "crates/sabia-cli"]
|
members = ["crates/sabia-core", "crates/sabia-cli"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
||||||
@@ -15,3 +15,7 @@ lto = "thin"
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
strip = true
|
strip = true
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<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>
|
<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 too · 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"><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:
|
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:
|
||||||
@@ -68,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)**.
|
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.
|
- Pick the interface, toggle **monitor mode** and start/stop **capture** by button.
|
||||||
- Live **networks seen** table, with signal, channel and security.
|
- Live **networks seen** table, with signal, channel and security.
|
||||||
- An **authorized-use** notice always in view and automatic privilege elevation on startup.
|
- An **authorized-use** notice always in view and automatic privilege elevation on startup.
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<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>
|
<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 também em Windows · 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"><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`:
|
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`:
|
||||||
@@ -68,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)**.
|
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.
|
- 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.
|
- 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.
|
- Aviso de **uso autorizado** sempre à vista e elevação automática de privilégio ao abrir.
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
**English** · [Português](SECURITY.pt-BR.md)
|
**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).
|
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).
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
[English](SECURITY.md) · **Português**
|
[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).
|
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).
|
||||||
@@ -2538,7 +2538,7 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-core"
|
name = "sabia-core"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -2555,7 +2555,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-gui"
|
name = "sabia-gui"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"sabia-core",
|
"sabia-core",
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# App desktop (Tauri 2). Fica fora do workspace principal porque exige
|
# Desktop application (Tauri 2). Kept outside the main workspace because it
|
||||||
# as bibliotecas de WebView do sistema (WebKitGTK no Linux).
|
# requires system WebView libraries (WebKitGTK on Linux).
|
||||||
[package]
|
[package]
|
||||||
name = "sabia-gui"
|
name = "sabia-gui"
|
||||||
description = "Sabiá: interface gráfica de auditoria de Wi-Fi"
|
description = "Sabiá: graphical Wi-Fi security auditing interface"
|
||||||
version = "1.0.1"
|
version = "1.0.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
authors = ["Sabiá contributors"]
|
||||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
fn main() {
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=../../../logo.png");
|
||||||
|
let _ = std::fs::copy("../../../logo.png", "../ui/logo.png");
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
// Modo monitor e captura crua exigem administrador no Windows: o
|
// Monitor mode and raw frame capture require Administrator privileges on Windows:
|
||||||
// manifesto pede elevação (UAC) assim que o app abre.
|
// embed UAC elevation manifest at startup.
|
||||||
let manifest = include_str!("windows-app-manifest.xml");
|
let manifest = include_str!("windows-app-manifest.xml");
|
||||||
let attrs = tauri_build::Attributes::new()
|
let attrs = tauri_build::Attributes::new()
|
||||||
.windows_attributes(tauri_build::WindowsAttributes::new().app_manifest(manifest));
|
.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))]
|
#[cfg(not(windows))]
|
||||||
tauri_build::build();
|
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"]}}
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 9.4 KiB |
|
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>
|
|
||||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
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>
|
|
||||||
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 979 B |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 311 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 350 KiB |
@@ -1,4 +1,4 @@
|
|||||||
// 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")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
use sabia_core::engine::{Engine, ScanOptions};
|
use sabia_core::engine::{Engine, ScanOptions};
|
||||||
@@ -506,9 +506,9 @@ fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
|
|||||||
state.0.update_apply().map_err(|e| e.to_string())
|
state.0.update_apply().map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `true` quando o processo já tem privilégio suficiente pra tudo que o
|
/// `true` when the process already has sufficient privileges for all
|
||||||
/// Sabiá faz (modo monitor, captura crua). No Windows o manifesto já
|
/// Sabiá operations (monitor mode, raw capture). On Windows the manifest already
|
||||||
/// exige administrador antes mesmo do app abrir, então aqui é sempre `true`.
|
/// requires administrator privileges before launch, so this is always `true`.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn is_elevated() -> bool {
|
fn is_elevated() -> bool {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -521,9 +521,9 @@ fn is_elevated() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reabre o app com privilégios de administrador (Linux/macOS). O app novo
|
/// Relaunches the app with administrator privileges (Linux/macOS). The new instance
|
||||||
/// encerra este quando abre; se a autorização falhar ou for cancelada, a
|
/// terminates this one on startup; if authorization fails or is cancelled, the
|
||||||
/// janela continua aqui e a interface mostra o motivo.
|
/// window stays open and the UI displays the reason.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
@@ -610,9 +610,9 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta
|
/// Executable path accessible to root. Inside an AppImage, `current_exe` points
|
||||||
/// pra dentro de um ponto de montagem FUSE do usuário, inacessível ao root:
|
/// inside a user FUSE mount that root cannot access:
|
||||||
/// usa o próprio arquivo `.AppImage` (variável `APPIMAGE`).
|
/// use the `.AppImage` file itself (`APPIMAGE` environment variable).
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
fn elevated_exe() -> Result<std::path::PathBuf, String> {
|
fn elevated_exe() -> Result<std::path::PathBuf, String> {
|
||||||
if let Some(p) = std::env::var_os("APPIMAGE")
|
if let Some(p) = std::env::var_os("APPIMAGE")
|
||||||
@@ -628,8 +628,11 @@ fn main() {
|
|||||||
let engine = Arc::new(Engine::new());
|
let engine = Arc::new(Engine::new());
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.manage(State(engine))
|
.manage(State(engine))
|
||||||
.setup(|app| {
|
.setup(|_app| {
|
||||||
// Quando abrimos elevados a pedido do app normal, ele fecha o app antigo.
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
let app = _app;
|
||||||
|
|
||||||
|
// When launched elevated by the unprivileged instance, terminate the old process.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
if let Some(pid) = std::env::var("SABIA_REPLACE_PID")
|
if let Some(pid) = std::env::var("SABIA_REPLACE_PID")
|
||||||
.ok()
|
.ok()
|
||||||
@@ -637,7 +640,7 @@ fn main() {
|
|||||||
{
|
{
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(1200));
|
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 {
|
unsafe {
|
||||||
if libc::geteuid() == 0 {
|
if libc::geteuid() == 0 {
|
||||||
libc::kill(pid, libc::SIGTERM);
|
libc::kill(pid, libc::SIGTERM);
|
||||||
@@ -646,10 +649,10 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Captura crua e modo monitor exigem root: pede elevação assim
|
// Raw capture and monitor mode require root: request elevation as soon
|
||||||
// que o app abre, sem esperar o usuário procurar um botão. Se
|
// as the app launches, without waiting for the user to look for a button. If
|
||||||
// a autorização falhar, a janela continua aberta e a interface
|
// authorization fails, the window stays open and the UI
|
||||||
// mostra o aviso (com um botão pra tentar de novo).
|
// displays a warning banner (with a retry button).
|
||||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() {
|
if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() {
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Sabia",
|
"productName": "Sabiá",
|
||||||
"version": "1.0.1",
|
"version": "1.0.4",
|
||||||
"identifier": "dev.sabia.app",
|
"identifier": "dev.sabia.app",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
--text-3: #8e8e93;
|
--text-3: #8e8e93;
|
||||||
--line: rgba(0, 0, 0, 0.09);
|
--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: #b45309;
|
||||||
--accent-ink: #ffffff;
|
--accent-ink: #ffffff;
|
||||||
--accent-soft: rgba(180, 83, 9, 0.11);
|
--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: #0891b2;
|
||||||
--signal-soft: rgba(8, 145, 178, 0.12);
|
--signal-soft: rgba(8, 145, 178, 0.12);
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|||||||
.page { max-width: 1040px; margin: 0 auto; padding: 30px 36px 100px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); }
|
.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; } }
|
@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 { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||||
.topbar h1 { font-size: 21px; }
|
.topbar h1 { font-size: 21px; }
|
||||||
.topbar .sub { color: var(--text-2); font-size: 13px; margin-top: 2px; }
|
.topbar .sub { color: var(--text-2); font-size: 13px; margin-top: 2px; }
|
||||||
@@ -141,14 +141,14 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|||||||
.wave.scanning path.live { opacity: 1; }
|
.wave.scanning path.live { opacity: 1; }
|
||||||
.wave.scanning path.flat { opacity: 0; }
|
.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 { 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 svg { flex: none; margin-top: 2px; color: var(--warn); }
|
||||||
.notice.warn { background: var(--warn-soft); } .notice.warn svg { 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.bad { background: var(--bad-soft); } .notice.bad svg { color: var(--bad); }
|
||||||
.notice b { color: var(--text); }
|
.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 { 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; }
|
.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); }
|
.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); }
|
||||||
@@ -166,7 +166,7 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|||||||
.btn.live { background: var(--bad); color: #fff; }
|
.btn.live { background: var(--bad); color: #fff; }
|
||||||
.btn.live:hover { filter: brightness(1.06); }
|
.btn.live:hover { filter: brightness(1.06); }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ tabela */
|
/* ------------------------------------------------------------ table */
|
||||||
.result-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 4px 2px 10px; }
|
.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-summary { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
||||||
.result-head strong { font-size: 15px; }
|
.result-head strong { font-size: 15px; }
|
||||||
@@ -186,7 +186,7 @@ tbody tr:hover { background: var(--surface-2); }
|
|||||||
td.ssid { font-weight: 600; }
|
td.ssid { font-weight: 600; }
|
||||||
td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
|
td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
|
||||||
|
|
||||||
/* rede agrupada por ESSID; cada BSSID vira uma linha expansível */
|
/* network grouped by ESSID; each BSSID renders as an expandable row */
|
||||||
.caret-cell { width: 34px; padding-left: 10px; padding-right: 0; }
|
.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 { 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:hover { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }
|
||||||
@@ -227,7 +227,7 @@ 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 h2 { font-size: 17px; color: var(--text); margin-bottom: 6px; }
|
||||||
.empty-state p { font-size: 13.5px; max-width: 360px; margin: 0 auto; }
|
.empty-state p { font-size: 13.5px; max-width: 360px; margin: 0 auto; }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ configurações */
|
/* ------------------------------------------------------------ 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 { 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::backdrop { background: rgba(0, 0, 0, .35); backdrop-filter: blur(6px); }
|
||||||
dialog h2 { font-size: 20px; margin-bottom: 16px; }
|
dialog h2 { font-size: 20px; margin-bottom: 16px; }
|
||||||
@@ -238,7 +238,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
|||||||
.select option { background-color: var(--surface); color: var(--text); }
|
.select option { background-color: var(--surface); color: var(--text); }
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------ detalhes do alvo */
|
/* ------------------------------------------------------------ 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 { 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; }
|
.ssid-btn:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
||||||
.bssid-item { cursor: pointer; }
|
.bssid-item { cursor: pointer; }
|
||||||
@@ -300,13 +300,11 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
|||||||
.empty-inline p { margin: 0; font-size: 13px; }
|
.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 { 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); }
|
.auth-check input { width: 16px; height: 16px; accent-color: var(--accent); }
|
||||||
/* Colunas CSS em vez de grid: os cards de ataque têm alturas muito
|
/* CSS multi-column layout instead of grid: attack cards have very different
|
||||||
diferentes entre si (Deauth/Handshake são enormes, PMF/WPS são curtos).
|
heights (Deauth/Handshake are tall, PMF/WPS are short). In a standard
|
||||||
Num grid de 2 colunas comum, o par mais curto de uma linha deixava um
|
2-column grid, the shorter card in a row left a large empty gap underneath.
|
||||||
vão vazio enorme embaixo (a altura da linha é a do item mais alto, e
|
Multi-column masonry flow lets each card take only the height it needs
|
||||||
align-items:start não estica o curto pra preencher). Colunas fazem os
|
with the next card stacking right below. */
|
||||||
cards fluírem como alvenaria: cada um só ocupa a altura que precisa, o
|
|
||||||
próximo entra logo abaixo, sem buracos. */
|
|
||||||
.attack-grid { column-count: 2; column-gap: 12px; }
|
.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-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, .attack-wide { column-span: all; }
|
||||||
@@ -336,7 +334,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
|||||||
.crack-bar i { display: block; width: 35%; height: 100%; border-radius: 999px; background: var(--accent); animation: crack-slide 1.1s ease-in-out infinite; }
|
.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%; } }
|
@keyframes crack-slide { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
|
||||||
|
|
||||||
/* ------------------------------------------------------------ responsivo */
|
/* ------------------------------------------------------------ responsive */
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
|
.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; }
|
.sidebar { border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; align-items: center; overflow-x: auto; }
|
||||||
@@ -359,7 +357,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
|||||||
.detail-card, .attack-card { padding: 12px; }
|
.detail-card, .attack-card { padding: 12px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------- análise de canais */
|
/* --------------------------------------------------- channel analysis */
|
||||||
.top-actions { display: flex; align-items: center; gap: 10px; }
|
.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 { 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 { border: 0; border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; color: var(--text-2); background: transparent; cursor: pointer; }
|
||||||
@@ -401,7 +399,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
|||||||
.chan-aps { color: var(--text-3); font-size: 11px; white-space: nowrap; }
|
.chan-aps { color: var(--text-3); font-size: 11px; white-space: nowrap; }
|
||||||
.chan-best .chan-n, .chan-best .chan-aps { color: var(--signal); }
|
.chan-best .chan-n, .chan-best .chan-aps { color: var(--signal); }
|
||||||
|
|
||||||
/* ------------------------------------------------- saúde do espectro (SMART) */
|
/* ------------------------------------------------- spectrum health (SMART) */
|
||||||
.chan-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; margin: 2px 0 16px; }
|
.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 { 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 small { color: var(--text-3); font-size: 12px; }
|
||||||
@@ -424,7 +422,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
|||||||
.chan-table tr.chan-best-row td { color: var(--signal); font-weight: 600; }
|
.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); }
|
.mini-tag { font-size: 8.5px; padding: 1px 4px; border-radius: 4px; background: var(--warn-soft); color: var(--warn); }
|
||||||
|
|
||||||
/* ------------------------------------------------ clientes e linha do tempo */
|
/* ------------------------------------------------ clients and timeline */
|
||||||
.spark { display: inline-block; width: 62px; height: 16px; vertical-align: middle; margin-left: 6px; }
|
.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 path { fill: none; stroke: var(--signal); stroke-width: 1.4; }
|
||||||
.spark-big { display: block; width: 100%; height: 70px; margin: 10px 0 0; }
|
.spark-big { display: block; width: 100%; height: 70px; margin: 10px 0 0; }
|
||||||
@@ -439,7 +437,7 @@ dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; m
|
|||||||
.tl-probe { color: var(--signal); }
|
.tl-probe { color: var(--signal); }
|
||||||
.tl-detail { color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.tl-detail { color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
|
||||||
/* ------------------------------------------------------------- WPS e relatório */
|
/* ------------------------------------------------------------- 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; }
|
.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 { 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-dlg:not([open]) { display: none; }
|
||||||
|
|||||||
@@ -53,11 +53,11 @@ function applyTheme(t) {
|
|||||||
|
|
||||||
function updateThemePref(t) {
|
function updateThemePref(t) {
|
||||||
applyTheme(t);
|
applyTheme(t);
|
||||||
try { localStorage.setItem('sabia-theme', t); } catch { /* sem armazenamento */ }
|
try { localStorage.setItem('sabia-theme', t); } catch { /* storage unavailable */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
let themePref = 'auto';
|
let themePref = 'auto';
|
||||||
try { themePref = localStorage.getItem('sabia-theme') || 'auto'; } catch { /* sem armazenamento */ }
|
try { themePref = localStorage.getItem('sabia-theme') || 'auto'; } catch { /* storage unavailable */ }
|
||||||
applyTheme(themePref);
|
applyTheme(themePref);
|
||||||
|
|
||||||
document.querySelectorAll('[data-theme-pick]').forEach((b) => {
|
document.querySelectorAll('[data-theme-pick]').forEach((b) => {
|
||||||
@@ -67,7 +67,7 @@ document.querySelectorAll('[data-theme-pick]').forEach((b) => {
|
|||||||
try {
|
try {
|
||||||
const sort = localStorage.getItem('sabia-sort');
|
const sort = localStorage.getItem('sabia-sort');
|
||||||
if (sort && [...els.sortSel.options].some((o) => o.value === sort)) els.sortSel.value = sort;
|
if (sort && [...els.sortSel.options].some((o) => o.value === sort)) els.sortSel.value = sort;
|
||||||
} catch { /* sem armazenamento */ }
|
} catch { /* storage unavailable */ }
|
||||||
|
|
||||||
els.settingsBtn.addEventListener('click', () => {
|
els.settingsBtn.addEventListener('click', () => {
|
||||||
if (!els.settingsDlg.open) els.settingsDlg.showModal();
|
if (!els.settingsDlg.open) els.settingsDlg.showModal();
|
||||||
@@ -75,14 +75,14 @@ els.settingsBtn.addEventListener('click', () => {
|
|||||||
|
|
||||||
function applyLangPref() {
|
function applyLangPref() {
|
||||||
let pref = 'auto';
|
let pref = 'auto';
|
||||||
try { pref = localStorage.getItem('sabia-lang') || 'auto'; } catch { /* ignora */ }
|
try { pref = localStorage.getItem('sabia-lang') || 'auto'; } catch { /* ignore */ }
|
||||||
els.langSel.value = pref;
|
els.langSel.value = pref;
|
||||||
setLang(pref === 'auto' ? undefined : pref);
|
setLang(pref === 'auto' ? undefined : pref);
|
||||||
}
|
}
|
||||||
|
|
||||||
els.langSel.addEventListener('change', () => {
|
els.langSel.addEventListener('change', () => {
|
||||||
const pref = els.langSel.value;
|
const pref = els.langSel.value;
|
||||||
try { localStorage.setItem('sabia-lang', pref); } catch { /* ignora */ }
|
try { localStorage.setItem('sabia-lang', pref); } catch { /* ignore */ }
|
||||||
setLang(pref === 'auto' ? undefined : pref);
|
setLang(pref === 'auto' ? undefined : pref);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,12 +90,12 @@ let ifaces = [];
|
|||||||
let current = null;
|
let current = null;
|
||||||
let scanning = false;
|
let scanning = false;
|
||||||
|
|
||||||
// Ordem de chegada estável por rede/BSSID: sem isso, cada poll reordenava a
|
// Stable arrival order per network/BSSID: without this, each poll resorted the
|
||||||
// lista pelo sinal instantâneo e as linhas ficavam "flutuando" de posição a
|
// list by instantaneous RSSI and rows jumped around on every
|
||||||
// cada atualização, tornando impossível clicar no alvo certo. A ordem de
|
// update, making it hard to click a target. Arrival order
|
||||||
// chegada serve de critério de desempate (e de ordenação padrão), então um
|
// serves as the tiebreaker (and default sort), so an
|
||||||
// item só muda de posição se o usuário trocar o modo de ordenação de
|
// item only moves when the user explicitly changes the sort mode
|
||||||
// verdade — não por uma flutuação de -1 dBm entre um poll e outro.
|
// rather than on a 1 dBm RSSI fluctuation between polls.
|
||||||
const firstSeenOrder = new Map();
|
const firstSeenOrder = new Map();
|
||||||
let firstSeenSeq = 0;
|
let firstSeenSeq = 0;
|
||||||
function orderOf(key) {
|
function orderOf(key) {
|
||||||
@@ -115,8 +115,8 @@ async function api(path, opts) {
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// No app desktop não existe servidor HTTP: as mesmas rotas viram chamadas
|
// In the desktop app there is no HTTP server: the same routes map to direct
|
||||||
// IPC diretas pro núcleo, pelos comandos registrados em src-tauri/src/main.rs.
|
// IPC calls into the core via the Tauri commands in src-tauri/src/main.rs.
|
||||||
async function tauriApi(path, opts = {}) {
|
async function tauriApi(path, opts = {}) {
|
||||||
const [p, qs] = path.split('?');
|
const [p, qs] = path.split('?');
|
||||||
const q = new URLSearchParams(qs || '');
|
const q = new URLSearchParams(qs || '');
|
||||||
@@ -280,8 +280,8 @@ function renderEmptyResult(message, hint) {
|
|||||||
els.resultArea.innerHTML = `<div class="ap-card empty-state">${ICON_EMPTY}<h2>${message}</h2><p>${hint}</p></div>`;
|
els.resultArea.innerHTML = `<div class="ap-card empty-state">${ICON_EMPTY}<h2>${message}</h2><p>${hint}</p></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Só troca o HTML quando muda de verdade: o poll de 1 s não deve
|
// Only update innerHTML when content actually changes: the 1s poll must not
|
||||||
// reconstruir a tabela (e perder foco/hover) à toa.
|
// rebuild the DOM table (and drop focus/hover) unnecessarily.
|
||||||
function setResultsHtml(html) {
|
function setResultsHtml(html) {
|
||||||
if (els.resultArea._h === html) return;
|
if (els.resultArea._h === html) return;
|
||||||
els.resultArea._h = html;
|
els.resultArea._h = html;
|
||||||
@@ -352,9 +352,9 @@ els.btnScan.addEventListener('click', async () => {
|
|||||||
if (!scanning) {
|
if (!scanning) {
|
||||||
const res = await api(`/api/scan/start?iface=${encodeURIComponent(current)}`, { method: 'POST' });
|
const res = await api(`/api/scan/start?iface=${encodeURIComponent(current)}`, { method: 'POST' });
|
||||||
if (res && res.ok === false) {
|
if (res && res.ok === false) {
|
||||||
// Sem isso, uma falha ao abrir a captura (permissão, interface saiu do
|
// Without this, a failure to start capture (permissions, interface left
|
||||||
// modo monitor, etc.) ficava muda: o botão virava "parar captura" e a
|
// monitor mode, etc.) was silent: the button flipped to "stop capture" and
|
||||||
// tela mostrava "escutando o ar" pra sempre, sem nenhuma rede chegar.
|
// the screen showed "listening" forever without any networks arriving.
|
||||||
alert('Não foi possível iniciar a captura: ' + (res.error || 'erro desconhecido'));
|
alert('Não foi possível iniciar a captura: ' + (res.error || 'erro desconhecido'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -398,7 +398,7 @@ function signalOf(ap) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function signalBars(dbm) {
|
function signalBars(dbm) {
|
||||||
// dBm tipicamente entre -30 (ótimo) e -90 (péssimo)
|
// dBm typically ranges from -30 (excellent) to -90 (weak)
|
||||||
let level = 0;
|
let level = 0;
|
||||||
if (dbm != null) {
|
if (dbm != null) {
|
||||||
if (dbm >= -55) level = 4;
|
if (dbm >= -55) level = 4;
|
||||||
@@ -414,9 +414,9 @@ function signalBars(dbm) {
|
|||||||
return `<span class="signal-bars">${bars}</span>`;
|
return `<span class="signal-bars">${bars}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agrupa por ESSID: uma rede pode ter vários BSSIDs (rádios) e todos
|
// Group by ESSID: a network may have multiple BSSIDs (radios) belonging
|
||||||
// pertencem ao mesmo alvo. Redes ocultas ficam separadas, pois sem SSID
|
// to the same target. Hidden networks stay separate since without an SSID
|
||||||
// não há como afirmar que dois BSSIDs são a mesma rede.
|
// two BSSIDs cannot be assumed to belong to the same network.
|
||||||
function buildGroups(aps) {
|
function buildGroups(aps) {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
for (const ap of aps) {
|
for (const ap of aps) {
|
||||||
@@ -460,9 +460,9 @@ function filterGroups(groups) {
|
|||||||
g.aps.some((a) => a.bssid.toLowerCase().includes(q)));
|
g.aps.some((a) => a.bssid.toLowerCase().includes(q)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dessensibiliza o sinal antes de comparar: arredondar pro m\u00faltiplo de 4
|
// Quantize RSSI before comparing: rounding to the nearest multiple of 4
|
||||||
// dBm mais pr\u00f3ximo evita que uma oscila\u00e7\u00e3o de 1-2 dBm entre dois polls
|
// dBm prevents a 1-2 dBm fluctuation between polls from
|
||||||
// inverta a ordem de dois alvos com sinal parecido a cada atualiza\u00e7\u00e3o.
|
// flipping the order of two targets with similar signal strength.
|
||||||
function signalBucket(v) {
|
function signalBucket(v) {
|
||||||
return v == null ? -1000 : Math.round(v / 4) * 4;
|
return v == null ? -1000 : Math.round(v / 4) * 4;
|
||||||
}
|
}
|
||||||
@@ -535,7 +535,7 @@ function renderGroup(g) {
|
|||||||
let lastAps = [];
|
let lastAps = [];
|
||||||
const openGroups = new Set();
|
const openGroups = new Set();
|
||||||
let viewMode = 'nets';
|
let viewMode = 'nets';
|
||||||
try { viewMode = localStorage.getItem('sabia-view') || 'nets'; } catch { /* sem armazenamento */ }
|
try { viewMode = localStorage.getItem('sabia-view') || 'nets'; } catch { /* storage unavailable */ }
|
||||||
let channelBand = null;
|
let channelBand = null;
|
||||||
let reportState = { iface: null, pages: 1, zoom: 1, url: null };
|
let reportState = { iface: null, pages: 1, zoom: 1, url: null };
|
||||||
let reportMeasureTimer = null;
|
let reportMeasureTimer = null;
|
||||||
@@ -632,8 +632,8 @@ function spectrumSvg(band) {
|
|||||||
|
|
||||||
const CHAN_TONE_WORD = { ok: 'Boa', warn: 'Atenção', bad: 'Saturado' };
|
const CHAN_TONE_WORD = { ok: 'Boa', warn: 'Atenção', bad: 'Saturado' };
|
||||||
|
|
||||||
/** Estado geral da banda: mesma lógica de "saúde" usada nos tons por canal,
|
/** Overall band status: applies the per-channel health logic to the
|
||||||
* só que aplicada ao canal recomendado — é o que decide o tom do card inteiro. */
|
* recommended channel to determine the summary card tone. */
|
||||||
function bandHealth(band, rec) {
|
function bandHealth(band, rec) {
|
||||||
if (!rec) return { tone: 'bad', label: 'Sem canal livre nessa banda' };
|
if (!rec) return { tone: 'bad', label: 'Sem canal livre nessa banda' };
|
||||||
if (rec.score >= 1.5) return { tone: 'bad', label: 'Até o melhor canal está saturado' };
|
if (rec.score >= 1.5) return { tone: 'bad', label: 'Até o melhor canal está saturado' };
|
||||||
@@ -752,7 +752,7 @@ function renderResults(aps) {
|
|||||||
|
|
||||||
function setView(mode) {
|
function setView(mode) {
|
||||||
viewMode = mode === 'channels' ? 'channels' : 'nets';
|
viewMode = mode === 'channels' ? 'channels' : 'nets';
|
||||||
try { localStorage.setItem('sabia-view', viewMode); } catch { /* ignora */ }
|
try { localStorage.setItem('sabia-view', viewMode); } catch { /* ignore */ }
|
||||||
document.querySelectorAll('#viewNav [data-view]').forEach((b) => {
|
document.querySelectorAll('#viewNav [data-view]').forEach((b) => {
|
||||||
b.setAttribute('aria-pressed', String(b.dataset.view === viewMode));
|
b.setAttribute('aria-pressed', String(b.dataset.view === viewMode));
|
||||||
});
|
});
|
||||||
@@ -779,7 +779,7 @@ els.resultArea.addEventListener('click', (e) => {
|
|||||||
|
|
||||||
els.searchInput.addEventListener('input', () => renderResults(lastAps));
|
els.searchInput.addEventListener('input', () => renderResults(lastAps));
|
||||||
els.sortSel.addEventListener('change', () => {
|
els.sortSel.addEventListener('change', () => {
|
||||||
try { localStorage.setItem('sabia-sort', els.sortSel.value); } catch { /* sem armazenamento */ }
|
try { localStorage.setItem('sabia-sort', els.sortSel.value); } catch { /* storage unavailable */ }
|
||||||
renderResults(lastAps);
|
renderResults(lastAps);
|
||||||
});
|
});
|
||||||
els.resultArea.addEventListener('click', (e) => {
|
els.resultArea.addEventListener('click', (e) => {
|
||||||
@@ -803,7 +803,7 @@ els.resultArea.addEventListener('keydown', (e) => {
|
|||||||
openTarget(target.dataset.bssid);
|
openTarget(target.dataset.bssid);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------- alvo
|
// ---------------------------------------------------------------- target
|
||||||
let lastSnapshot = null;
|
let lastSnapshot = null;
|
||||||
let selectedBssid = null;
|
let selectedBssid = null;
|
||||||
let activeTab = 'overview';
|
let activeTab = 'overview';
|
||||||
@@ -1056,8 +1056,8 @@ const EVENT_LABEL = {
|
|||||||
deauth: 'desautenticado',
|
deauth: 'desautenticado',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Sparkline em tamanho real (não o mini de 62px da tabela), pra caber bem
|
/** Full-size RSSI sparkline (wider than the 62px table mini-chart) for the
|
||||||
* sozinho dentro do card de sinal do detalhe do cliente. */
|
* client detail signal card. */
|
||||||
function bigSparkline(history) {
|
function bigSparkline(history) {
|
||||||
const points = (history || []).slice(-60);
|
const points = (history || []).slice(-60);
|
||||||
if (points.length < 2) return '<p class="hint">Sem amostras de sinal suficientes ainda.</p>';
|
if (points.length < 2) return '<p class="hint">Sem amostras de sinal suficientes ainda.</p>';
|
||||||
@@ -1077,9 +1077,8 @@ function bigSparkline(history) {
|
|||||||
return `<svg class="spark spark-big" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true"><path d="${d}"/></svg>`;
|
return `<svg class="spark spark-big" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true"><path d="${d}"/></svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Visão completa de um cliente: tudo que o Sabiá já capturou sobre ele —
|
/** Detailed client view: consolidates everything captured about a station —
|
||||||
* identificação, rede, sinal e a linha do tempo de eventos — num painel só,
|
* identity hints, associated AP, signal history, and event timeline — in one modal. */
|
||||||
* em vez de ter que adivinhar olhando a linha compacta da tabela. */
|
|
||||||
function renderClientDetail(clients) {
|
function renderClientDetail(clients) {
|
||||||
const client = clients.find((c) => c.mac === attack.timelineMac);
|
const client = clients.find((c) => c.mac === attack.timelineMac);
|
||||||
if (!client) return '';
|
if (!client) return '';
|
||||||
@@ -1522,11 +1521,11 @@ async function loadSnapshot() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enquanto startDeauthJob está religando a captura e esperando o AP
|
// While startDeauthJob is restarting capture and waiting for the AP
|
||||||
// reaparecer na varredura, a sessão nova responde vazia por alguns ciclos
|
// to reappear in the scan, the fresh session returns empty for a few poll
|
||||||
// de poll — sem essa trava, renderTarget() interpreta "AP não visto ainda"
|
// cycles — without this guard, renderTarget() treats "AP not seen yet"
|
||||||
// como "AP não existe mais" e fecha o diálogo debaixo do usuário no meio
|
// as "AP gone" and closes the modal underneath the user mid-action.
|
||||||
// da própria ação que ele acabou de pedir.
|
//
|
||||||
let resumingForBssid = null;
|
let resumingForBssid = null;
|
||||||
|
|
||||||
function renderTarget(force) {
|
function renderTarget(force) {
|
||||||
@@ -1671,13 +1670,13 @@ async function waitForBssidChannel(bssid, timeoutMs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startDeauthJob(handshake) {
|
async function startDeauthJob(handshake) {
|
||||||
// Fixa o alvo no início: o resto desta função faz esperas longas
|
// Snapshot the target BSSID upfront: the rest of this function awaits
|
||||||
// (religar a captura, aguardar a rede reaparecer na varredura), e nesse
|
// capture restart and AP rediscovery, during which the global poll could
|
||||||
// meio tempo o poll global pode não achar mais o AP na primeira resposta
|
// see an empty initial snapshot and close the modal, clearing
|
||||||
// vazia da sessão recém-reiniciada e fechar o diálogo, zerando
|
// `selectedBssid`.
|
||||||
// `selectedBssid`. Sem essa cópia local, o job acabava sendo disparado
|
//
|
||||||
// com bssid=null ("MAC inválido: null") mesmo depois de localizar a rede
|
//
|
||||||
// com sucesso.
|
//
|
||||||
const bssid = selectedBssid;
|
const bssid = selectedBssid;
|
||||||
const station = document.getElementById('deauthStation')?.value || '';
|
const station = document.getElementById('deauthStation')?.value || '';
|
||||||
const direction = document.getElementById('deauthDirection')?.value || attack.direction;
|
const direction = document.getElementById('deauthDirection')?.value || attack.direction;
|
||||||
@@ -1688,10 +1687,10 @@ async function startDeauthJob(handshake) {
|
|||||||
setResult('deauthResult', 'Escolha um cliente para desautenticar nas direções sta/both.', true);
|
setResult('deauthResult', 'Escolha um cliente para desautenticar nas direções sta/both.', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Essa ação exige uma sessão de captura ativa (é o que guarda o handshake
|
// This action requires an active capture session (which stores the assembled
|
||||||
// montado); sem isso o backend recusa com "inicie a captura antes do
|
// handshake); rather than forcing the user to manually restart capture,
|
||||||
// job". Em vez de obrigar o usuário a voltar e religar manualmente, retoma
|
// resume capture automatically here.
|
||||||
// a captura aqui — do jeito que ele descreveu querer.
|
//
|
||||||
if (!scanning) {
|
if (!scanning) {
|
||||||
const scanRes = await api(`/api/scan/start?iface=${encodeURIComponent(current)}`, { method: 'POST' });
|
const scanRes = await api(`/api/scan/start?iface=${encodeURIComponent(current)}`, { method: 'POST' });
|
||||||
if (scanRes && scanRes.ok === false) {
|
if (scanRes && scanRes.ok === false) {
|
||||||
@@ -1702,12 +1701,12 @@ async function startDeauthJob(handshake) {
|
|||||||
els.btnScan.textContent = 'parar captura';
|
els.btnScan.textContent = 'parar captura';
|
||||||
els.btnScan.classList.add('live');
|
els.btnScan.classList.add('live');
|
||||||
els.wave.classList.add('scanning');
|
els.wave.classList.add('scanning');
|
||||||
// A captura acabou de reiniciar: a sessão ainda não sabe em qual canal
|
// Capture just restarted: the session does not yet know which channel
|
||||||
// este BSSID está, e o job travaria o canal errado (o canal em que o
|
// this BSSID is on, so starting the job immediately would lock the wrong
|
||||||
// scanner por acaso já estava saltando) em vez do canal real do AP —
|
// channel. Wait briefly for the AP to reappear in the scan snapshot
|
||||||
// os quadros de deauth saem no vácuo e o job termina sem efeito. Espera
|
// before launching the job.
|
||||||
// o AP reaparecer no snapshot (ele já esteve no ar, é questão de
|
//
|
||||||
// segundos de varredura) antes de disparar o job.
|
//
|
||||||
setResult('deauthResult', 'Retomando a captura e localizando a rede…');
|
setResult('deauthResult', 'Retomando a captura e localizando a rede…');
|
||||||
resumingForBssid = bssid;
|
resumingForBssid = bssid;
|
||||||
let found;
|
let found;
|
||||||
@@ -2370,10 +2369,9 @@ async function poll() {
|
|||||||
if (scanning && current) {
|
if (scanning && current) {
|
||||||
const snap = await api(`/api/scan/snapshot?iface=${encodeURIComponent(current)}`);
|
const snap = await api(`/api/scan/snapshot?iface=${encodeURIComponent(current)}`);
|
||||||
if (snap?.access_points && snap.running === false) {
|
if (snap?.access_points && snap.running === false) {
|
||||||
// A sessão existe e respondeu normal (200), mas o próprio engine
|
// The session exists and returned HTTP 200, but the engine marked
|
||||||
// marcou running=false: a thread de captura morreu sozinha (erro de
|
// running=false because the capture thread died on its own (I/O error,
|
||||||
// I/O, adaptador caiu da USB, etc.). Sem isso, a UI continuava achando
|
// USB adapter unplugged, etc.). Surface the actual reason to the UI.
|
||||||
// que ainda estava capturando e nunca contava o motivo real.
|
|
||||||
scanning = false;
|
scanning = false;
|
||||||
els.btnScan.textContent = 'iniciar captura';
|
els.btnScan.textContent = 'iniciar captura';
|
||||||
els.btnScan.classList.remove('live');
|
els.btnScan.classList.remove('live');
|
||||||
@@ -2386,10 +2384,10 @@ async function poll() {
|
|||||||
renderResults(snap.access_points);
|
renderResults(snap.access_points);
|
||||||
if (selectedBssid) renderTarget(false);
|
if (selectedBssid) renderTarget(false);
|
||||||
} else if (snap && snap.ok === false) {
|
} else if (snap && snap.ok === false) {
|
||||||
// A captura morreu no meio (interface saiu do monitor, processo externo
|
// Capture stopped mid-flight (interface left monitor mode, external
|
||||||
// tomou o adaptador, etc.). Antes disso caía no fallback de baixo, que
|
// process claimed the adapter, etc.). Without this branch, the fallback
|
||||||
// sempre responde 200 com lista vazia — a UI continuava "escutando o
|
// below returns 200 with an empty list and the UI keeps listening
|
||||||
// ar" indefinidamente sem nenhuma rede aparecer e sem avisar por quê.
|
// indefinitely without any network appearing or explaining why.
|
||||||
scanning = false;
|
scanning = false;
|
||||||
els.btnScan.textContent = 'iniciar captura';
|
els.btnScan.textContent = 'iniciar captura';
|
||||||
els.btnScan.classList.remove('live');
|
els.btnScan.classList.remove('live');
|
||||||
@@ -2405,7 +2403,7 @@ async function poll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkElevation() {
|
async function checkElevation() {
|
||||||
if (!tauri) return; // via `sabia ui` a elevação é responsabilidade do terminal (sudo)
|
if (!tauri) return; // in `sabia ui` mode privilege elevation is handled by the terminal (sudo)
|
||||||
const notice = document.getElementById('elevateNotice');
|
const notice = document.getElementById('elevateNotice');
|
||||||
const msg = document.getElementById('elevateMsg');
|
const msg = document.getElementById('elevateMsg');
|
||||||
const retry = document.getElementById('btnElevateRetry');
|
const retry = document.getElementById('btnElevateRetry');
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// 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 = {
|
export const EN = {
|
||||||
// index.html / sidebar
|
// index.html / sidebar
|
||||||
'auditoria de Wi-Fi': 'Wi-Fi auditing',
|
'auditoria de Wi-Fi': 'Wi-Fi auditing',
|
||||||
@@ -19,7 +19,7 @@ export const EN = {
|
|||||||
'modo monitor': 'monitor mode',
|
'modo monitor': 'monitor mode',
|
||||||
'modo gerenciado': 'managed mode',
|
'modo gerenciado': 'managed mode',
|
||||||
|
|
||||||
// elevação / avisos
|
// elevation / notices
|
||||||
'Pedindo privilégio de administrador — necessário pra modo monitor e captura…':
|
'Pedindo privilégio de administrador — necessário pra modo monitor e captura…':
|
||||||
'Requesting administrator privilege — needed for monitor mode and capture…',
|
'Requesting administrator privilege — needed for monitor mode and capture…',
|
||||||
'tentar de novo': 'try again',
|
'tentar de novo': 'try again',
|
||||||
@@ -27,13 +27,13 @@ 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 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.',
|
'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',
|
'ativar modo monitor': 'enable monitor mode',
|
||||||
'desativar modo monitor': 'disable monitor mode',
|
'desativar modo monitor': 'disable monitor mode',
|
||||||
'iniciar captura': 'start capture',
|
'iniciar captura': 'start capture',
|
||||||
'parar captura': 'stop capture',
|
'parar captura': 'stop capture',
|
||||||
|
|
||||||
// resultados
|
// results
|
||||||
'Redes': 'Networks',
|
'Redes': 'Networks',
|
||||||
'Redes vistas': 'Networks seen',
|
'Redes vistas': 'Networks seen',
|
||||||
'Linha do tempo': 'Timeline',
|
'Linha do tempo': 'Timeline',
|
||||||
@@ -72,7 +72,7 @@ export const EN = {
|
|||||||
'Nenhuma rede corresponde ao filtro': 'No network matches the filter',
|
'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.',
|
'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 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 iniciar a captura:': 'Could not start the capture:',
|
||||||
'Não foi possível retomar a captura: {n}': 'Could not resume the capture: {n}',
|
'Não foi possível retomar a captura: {n}': 'Could not resume the capture: {n}',
|
||||||
@@ -82,7 +82,7 @@ export const EN = {
|
|||||||
'erro desconhecido': 'unknown error',
|
'erro desconhecido': 'unknown error',
|
||||||
'rota desconhecida': 'unknown route',
|
'rota desconhecida': 'unknown route',
|
||||||
|
|
||||||
// seletor de idioma / configurações
|
// language selector / settings
|
||||||
'Configurações': 'Settings',
|
'Configurações': 'Settings',
|
||||||
'Tema': 'Theme',
|
'Tema': 'Theme',
|
||||||
'Claro': 'Light',
|
'Claro': 'Light',
|
||||||
@@ -133,7 +133,7 @@ export const EN = {
|
|||||||
'quantidade de quadros deve ficar entre 1 e 256': 'frame count must be between 1 and 256',
|
'quantidade de quadros deve ficar entre 1 e 256': 'frame count must be between 1 and 256',
|
||||||
'MAC inválido: {n}': 'invalid MAC: {n}',
|
'MAC inválido: {n}': 'invalid MAC: {n}',
|
||||||
|
|
||||||
// detalhes do alvo e ataques
|
// target details and attacks
|
||||||
'não': 'no',
|
'não': 'no',
|
||||||
'não anunciado': 'not advertised',
|
'não anunciado': 'not advertised',
|
||||||
'quebrável': 'crackable',
|
'quebrável': 'crackable',
|
||||||
@@ -340,7 +340,7 @@ export const EN = {
|
|||||||
'{n} · canal {n}': '{n} · channel {n}',
|
'{n} · canal {n}': '{n} · channel {n}',
|
||||||
'{n} ({n})': '{n} ({n})',
|
'{n} ({n})': '{n} ({n})',
|
||||||
|
|
||||||
// canais, PMKID/PMF, WPS e relatório
|
// channels, PMKID/PMF, WPS, and report
|
||||||
'Ocupação dos canais em {n}': 'Channel occupancy on {n}',
|
'Ocupação dos canais em {n}': 'Channel occupancy on {n}',
|
||||||
'Sem dados de canal ainda': 'No channel data yet',
|
'Sem dados de canal ainda': 'No channel data yet',
|
||||||
'Inicie a captura para o Sabiá medir ocupação e sugerir canais.':
|
'Inicie a captura para o Sabiá medir ocupação e sugerir canais.':
|
||||||
@@ -358,7 +358,7 @@ export const EN = {
|
|||||||
'Melhor canal': 'Best channel',
|
'Melhor canal': 'Best channel',
|
||||||
'Espectro e canais': 'Spectrum and channels',
|
'Espectro e canais': 'Spectrum and channels',
|
||||||
|
|
||||||
// saúde do espectro (canais, estilo SMART)
|
// spectrum health (channels, SMART style)
|
||||||
'Boa': 'Good',
|
'Boa': 'Good',
|
||||||
'Atenção': 'Attention',
|
'Atenção': 'Attention',
|
||||||
'Saturado': 'Saturated',
|
'Saturado': 'Saturated',
|
||||||
@@ -478,7 +478,7 @@ export const EN = {
|
|||||||
'não foi possível executar {n}: {n}': 'could not run {n}: {n}',
|
'não foi possível executar {n}: {n}': 'could not run {n}: {n}',
|
||||||
'{n} terminou com código {n}': '{n} exited with code {n}',
|
'{n} terminou com código {n}': '{n} exited with code {n}',
|
||||||
|
|
||||||
// achados do WIDS (analysis.rs)
|
// WIDS findings (analysis.rs)
|
||||||
'SSID “{n}” com seguranças diferentes': 'SSID “{n}” with different security levels',
|
'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.':
|
'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.',
|
'The same name appears on BSSIDs with different security levels: {n}. It may be an evil twin or a misconfigured AP.',
|
||||||
@@ -525,7 +525,7 @@ export const EN = {
|
|||||||
'The station did not reassociate in the observed window. PMF may be active, or the client simply stayed idle.',
|
'The station did not reassociate in the observed window. PMF may be active, or the client simply stayed idle.',
|
||||||
'Inconclusivo': 'Inconclusive',
|
'Inconclusivo': 'Inconclusive',
|
||||||
|
|
||||||
// deauth contínuo e identificação de clientes
|
// 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.':
|
'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.',
|
'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',
|
'Iniciar contínuo': 'Start continuous',
|
||||||
@@ -547,6 +547,8 @@ export const EN = {
|
|||||||
'The bundled reaver could not be extracted in this run.',
|
'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 (vazio = embutido, 9,7 milhões de senhas)': 'Wordlist (empty = bundled, 9.7 million passwords)',
|
||||||
'dicionário próprio, opcional': 'custom wordlist, optional',
|
'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',
|
'Dispositivo': 'Device',
|
||||||
'MAC aleatório': 'Random MAC',
|
'MAC aleatório': 'Random MAC',
|
||||||
'Cliente {n}': 'Client {n}',
|
'Cliente {n}': 'Client {n}',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Idioma da interface. O texto-fonte é em português; no modo inglês um tradutor
|
// UI language. Source strings are in Portuguese; in English mode a translator
|
||||||
// troca os textos exibidos (nós de texto e atributos) usando um dicionário, e
|
// replaces displayed strings (text nodes and attributes) using a dictionary, and
|
||||||
// continua observando a página para traduzir o que for desenhado depois.
|
// keeps observing the page to translate dynamically rendered elements.
|
||||||
// Textos com valores variáveis usam {n} no dicionário (ex.: "{n} redes" -> "{n} networks").
|
// Variable placeholders use {n} in the dictionary (e.g. "{n} networks").
|
||||||
|
|
||||||
import { EN } from './i18n-en.js';
|
import { EN } from './i18n-en.js';
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export function resolveLang(pref) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
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 SKIP = '.ssid, .bssid, .mono, pre, code, input, textarea, script, style';
|
||||||
|
|
||||||
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
|
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, '\\$&'));
|
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v };
|
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);
|
.sort((a, b) => b.re.source.length - a.re.source.length);
|
||||||
|
|
||||||
const cache = new Map();
|
const cache = new Map();
|
||||||
@@ -43,7 +43,7 @@ export function tr(text) {
|
|||||||
const m = core.match(p.re);
|
const m = core.match(p.re);
|
||||||
if (m) {
|
if (m) {
|
||||||
let i = 0;
|
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] ?? ''));
|
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export function tr(text) {
|
|||||||
return lead + out + trail;
|
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;
|
let observer = null;
|
||||||
|
|
||||||
function translateNode(n) {
|
function translateNode(n) {
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Sabiá</title>
|
<title>Sabiá</title>
|
||||||
<link rel="icon" href="/sabia.png">
|
<link rel="icon" href="/logo.png">
|
||||||
<link rel="stylesheet" href="/app.css">
|
<link rel="stylesheet" href="/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<img class="logo" src="/sabia.png" alt="">
|
<img class="logo" src="/logo.png" alt="">
|
||||||
<div>
|
<div>
|
||||||
<strong>Sabiá</strong>
|
<strong>Sabiá</strong>
|
||||||
<small>auditoria de Wi-Fi</small>
|
<small>auditoria de Wi-Fi</small>
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sabia-cli"
|
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
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use std::time::Duration;
|
|||||||
version
|
version
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
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")]
|
#[arg(long, global = true, default_value = ".sabia-audit.log")]
|
||||||
audit: PathBuf,
|
audit: PathBuf,
|
||||||
|
|
||||||
@@ -26,95 +26,95 @@ struct Cli {
|
|||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
/// Lista interfaces, canais e controla o modo monitor.
|
/// List interfaces, channels, and toggle monitor mode.
|
||||||
Iface {
|
Iface {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
action: IfaceAction,
|
action: IfaceAction,
|
||||||
},
|
},
|
||||||
/// Captura o ar e mostra redes, clientes e handshakes.
|
/// Capture live traffic and display networks, clients, and handshakes.
|
||||||
Scan {
|
Scan {
|
||||||
iface: String,
|
iface: String,
|
||||||
/// Canal fixo; sem isso, percorre os canais suportados.
|
/// Fixed channel; when omitted, hops across supported channels.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
channel: Option<u32>,
|
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)]
|
#[arg(long)]
|
||||||
band: Option<String>,
|
band: Option<String>,
|
||||||
/// Tempo em cada canal (ms).
|
/// Channel dwell time in milliseconds.
|
||||||
#[arg(long, default_value_t = 350)]
|
#[arg(long, default_value_t = 350)]
|
||||||
hop_ms: u64,
|
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)]
|
#[arg(long)]
|
||||||
seconds: Option<u64>,
|
seconds: Option<u64>,
|
||||||
/// Salva os quadros capturados em um .pcap.
|
/// Save captured frames to a `.pcap` file.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
out: Option<PathBuf>,
|
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")]
|
#[arg(long, value_name = "DICIONARIO")]
|
||||||
crack: Option<PathBuf>,
|
crack: Option<PathBuf>,
|
||||||
/// Mostra apenas o JSON final.
|
/// Output only the final JSON summary.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Analisa um .pcap offline (redes, clientes e handshakes).
|
/// Analyze a `.pcap` file offline (networks, clients, and handshakes).
|
||||||
Analyze {
|
Analyze {
|
||||||
pcap: PathBuf,
|
pcap: PathBuf,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Quebra um handshake de um .pcap por dicionário.
|
/// Run a dictionary attack against a handshake in a `.pcap` file.
|
||||||
Crack {
|
Crack {
|
||||||
pcap: PathBuf,
|
pcap: PathBuf,
|
||||||
/// Dicionário com uma senha por linha.
|
/// Wordlist file (one candidate passphrase per line).
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
wordlist: PathBuf,
|
wordlist: PathBuf,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
bssid: Option<String>,
|
bssid: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
station: Option<String>,
|
station: Option<String>,
|
||||||
/// Aplica mutações comuns a cada palavra (maiúscula, sufixos
|
/// Apply common mutation rules to each word (capitalization, numeric
|
||||||
/// numéricos, leet simples) — multiplica o tempo de verdade.
|
/// suffixes, simple leetspeak) — multiplies total runtime.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
rules: bool,
|
rules: bool,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Envia desautenticação para um AP/estação (teste autorizado).
|
/// Send deauthentication frames to an AP/station (authorized testing only).
|
||||||
Deauth {
|
Deauth {
|
||||||
iface: String,
|
iface: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
bssid: String,
|
bssid: String,
|
||||||
/// Sem isso, o destino é broadcast.
|
/// Target client MAC; defaults to broadcast when omitted.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
station: Option<String>,
|
station: Option<String>,
|
||||||
#[arg(long, default_value_t = 5)]
|
#[arg(long, default_value_t = 5)]
|
||||||
count: u32,
|
count: u32,
|
||||||
#[arg(long, default_value_t = 7)]
|
#[arg(long, default_value_t = 7)]
|
||||||
reason: u16,
|
reason: u16,
|
||||||
/// Confirma que você tem autorização para testar esta rede.
|
/// Confirm explicit authorization to test this target network.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
yes: bool,
|
yes: bool,
|
||||||
},
|
},
|
||||||
/// Diagnóstico de privilégios, ferramentas e interfaces.
|
/// Diagnose privileges, external tools, and wireless interfaces.
|
||||||
Doctor {
|
Doctor {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Inspeciona a trilha de auditoria.
|
/// Inspect and verify the hash-chained audit trail.
|
||||||
Audit {
|
Audit {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
action: AuditAction,
|
action: AuditAction,
|
||||||
},
|
},
|
||||||
/// Verifica e aplica atualizações do próprio sabia.
|
/// Check for and apply `sabia` CLI updates.
|
||||||
Update {
|
Update {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
apply: bool,
|
apply: bool,
|
||||||
},
|
},
|
||||||
/// Abre a interface gráfica (servidor local + navegador).
|
/// Launch the web UI (local HTTP server + browser).
|
||||||
Ui {
|
Ui {
|
||||||
#[arg(long, default_value = "4337")]
|
#[arg(long, default_value = "4337")]
|
||||||
port: u16,
|
port: u16,
|
||||||
/// Não tenta abrir o navegador automaticamente.
|
/// Do not open the browser automatically.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
no_open: bool,
|
no_open: bool,
|
||||||
},
|
},
|
||||||
@@ -123,13 +123,13 @@ enum Cmd {
|
|||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum IfaceAction {
|
enum IfaceAction {
|
||||||
List,
|
List,
|
||||||
/// Mostra os canais suportados pelo rádio.
|
/// List channels supported by the wireless phy.
|
||||||
Channels {
|
Channels {
|
||||||
iface: String,
|
iface: String,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
},
|
},
|
||||||
/// Liga/desliga modo monitor numa interface Wi-Fi.
|
/// Enable or disable monitor mode on a Wi-Fi interface.
|
||||||
Monitor {
|
Monitor {
|
||||||
iface: String,
|
iface: String,
|
||||||
#[arg(value_enum)]
|
#[arg(value_enum)]
|
||||||
@@ -139,9 +139,9 @@ enum IfaceAction {
|
|||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum AuditAction {
|
enum AuditAction {
|
||||||
/// Confere a integridade da cadeia.
|
/// Verify the cryptographic hash chain integrity.
|
||||||
Verify { path: Option<PathBuf> },
|
Verify { path: Option<PathBuf> },
|
||||||
/// Mostra as últimas entradas.
|
/// Show the last N entries.
|
||||||
Show {
|
Show {
|
||||||
#[arg(long, default_value_t = 20)]
|
#[arg(long, default_value_t = 20)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Servidor local da interface: uma única página HTML/JS servida em
|
//! Local web UI server: serves the single-page HTML/JS app on
|
||||||
//! `127.0.0.1`, protegida por um token gerado na hora, falando com o
|
//! `127.0.0.1`, protected by a per-session random token, communicating with
|
||||||
//! núcleo por uma API JSON simples. A mesma página em `apps/gui/ui` é o
|
//! the core over JSON endpoints. The exact same frontend in `apps/gui/ui`
|
||||||
//! que o app desktop (Tauri) carrega — só muda o transporte.
|
//! powers the Tauri desktop app.
|
||||||
|
|
||||||
use sabia_core::engine::{Engine, ScanOptions};
|
use sabia_core::engine::{Engine, ScanOptions};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -17,19 +17,19 @@ const APP_CSS: &str = include_str!("../../../../apps/gui/ui/app.css");
|
|||||||
const APP_JS: &str = include_str!("../../../../apps/gui/ui/app.js");
|
const APP_JS: &str = include_str!("../../../../apps/gui/ui/app.js");
|
||||||
const I18N_JS: &str = include_str!("../../../../apps/gui/ui/i18n.js");
|
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 I18N_EN_JS: &str = include_str!("../../../../apps/gui/ui/i18n-en.js");
|
||||||
const LOGO_PNG: &[u8] = include_bytes!("../../../../apps/gui/ui/sabia.png");
|
const LOGO_PNG: &[u8] = include_bytes!("../../../../logo.png");
|
||||||
const PAGED_JS: &str = include_str!("../../../../apps/gui/ui/paged.polyfill.min.js");
|
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<()> {
|
pub fn serve(port: u16, audit_path: std::path::PathBuf, open: bool) -> sabia_core::Result<()> {
|
||||||
let token = gen_token();
|
let token = gen_token();
|
||||||
let listener = TcpListener::bind(("127.0.0.1", port))?;
|
let listener = TcpListener::bind(("127.0.0.1", port))?;
|
||||||
let engine = Arc::new(Engine::with_audit_path(audit_path));
|
let engine = Arc::new(Engine::with_audit_path(audit_path));
|
||||||
|
|
||||||
// Sem isso, Ctrl+C (ou um `kill` normal) durante um Evil Twin ativo
|
// Without a signal handler, Ctrl+C (or `SIGTERM`) during an active Evil Twin
|
||||||
// mata o processo sem rodar a limpeza: hostapd/dnsmasq ficam órfãos
|
// exits before cleanup runs: `hostapd`/`dnsmasq` are left orphaned
|
||||||
// e as duas interfaces ficam presas em `managed no` pro
|
// and both interfaces remain stuck in `managed no` under
|
||||||
// NetworkManager — o painel de Wi-Fi do sistema some por completo até
|
// NetworkManager until the next engine startup runs self-healing.
|
||||||
// o próximo início do engine se auto-curar.
|
//
|
||||||
{
|
{
|
||||||
let engine = engine.clone();
|
let engine = engine.clone();
|
||||||
let _ = ctrlc::set_handler(move || {
|
let _ = ctrlc::set_handler(move || {
|
||||||
@@ -275,7 +275,7 @@ fn handle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
match req.path.as_str() {
|
match req.path.as_str() {
|
||||||
"/paged.polyfill.min.js" => {
|
"/vendor/paged.polyfill.min.js" | "/paged.polyfill.min.js" => {
|
||||||
return respond(
|
return respond(
|
||||||
&mut stream,
|
&mut stream,
|
||||||
200,
|
200,
|
||||||
@@ -308,7 +308,7 @@ fn handle(
|
|||||||
I18N_EN_JS.as_bytes(),
|
I18N_EN_JS.as_bytes(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"/sabia.png" => return respond(&mut stream, 200, "image/png", LOGO_PNG),
|
"/logo.png" => return respond(&mut stream, 200, "image/png", LOGO_PNG),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sabia-core"
|
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
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -25,5 +25,5 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"]
|
|||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||||
# captura crua (AF_PACKET) só existe no Linux; outras plataformas compilam
|
# Raw capture (AF_PACKET) is Linux-only; other platforms compile the crate
|
||||||
# a crate mas os módulos de captura ficam indisponíveis em tempo de execução.
|
# while live capture modules remain unavailable at runtime.
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
//! Análise de canais, detecção de anomalias (WIDS) e leitura de WPA3.
|
//! Channel utilization analysis, wireless anomaly detection (WIDS), and WPA3 indicators.
|
||||||
|
//!
|
||||||
|
//! Everything here is derived passively from the captured scan table without injecting frames.
|
||||||
|
//! Both the live UI and the standalone audit report consume this model.
|
||||||
//!
|
//!
|
||||||
//! Tudo aqui é derivado da tabela já capturada — nada injeta quadros. A
|
|
||||||
//! interface usa o resultado para desenhar o gráfico de canais e os alertas;
|
|
||||||
//! o relatório usa o mesmo modelo para não ter duas verdades.
|
|
||||||
|
|
||||||
use crate::radiotap::{self, Band};
|
use crate::radiotap::{self, Band};
|
||||||
use crate::scan::AccessPoint;
|
use crate::scan::AccessPoint;
|
||||||
@@ -280,8 +280,8 @@ pub fn analyze(aps: &[AccessPoint], deauth_60s: u32, now: u64) -> Analysis {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// SSID anunciado com seguranças diferentes é o sinal clássico de evil
|
// Same SSID advertised with mixed security suites (e.g. WPA2 alongside Open)
|
||||||
// twin — legítimo costuma manter o mesmo conjunto em todos os rádios.
|
// is a classic Evil Twin indicator.
|
||||||
let mut by_ssid: HashMap<String, Vec<&AccessPoint>> = HashMap::new();
|
let mut by_ssid: HashMap<String, Vec<&AccessPoint>> = HashMap::new();
|
||||||
for ap in aps {
|
for ap in aps {
|
||||||
if let Some(ssid) = ap.ssid.as_deref().filter(|s| !s.is_empty()) {
|
if let Some(ssid) = ap.ssid.as_deref().filter(|s| !s.is_empty()) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
//! Trilha de auditoria encadeada por hash de toda ação sensível (ligar
|
//! Hash-chained audit trail of every sensitive operation (enabling
|
||||||
//! modo monitor, iniciar captura, trocar canal, etc.) — mesmo princípio do
|
//! monitor mode, starting capture, changing channels, active attacks):
|
||||||
//! módulo `audit` do Guará: cada evento guarda o hash do anterior, então
|
//! each entry commits to the SHA-256 of the previous entry so any
|
||||||
//! qualquer edição retroativa do log quebra a cadeia e é detectável.
|
//! retroactive tampering breaks the chain.
|
||||||
|
//!
|
||||||
|
//! Provides verifiable engagement evidence of what was executed, when,
|
||||||
|
//! and against which target/interface.
|
||||||
|
//!
|
||||||
//!
|
//!
|
||||||
//! Isso existe para que o uso do Sabiá em um engajamento de pentest tenha
|
|
||||||
//! evidência de o quê foi feito, quando e contra qual rede/interface —
|
|
||||||
//! não para decidir sozinho se uma ação é autorizada (isso é sempre do
|
|
||||||
//! operador).
|
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -85,7 +85,7 @@ impl AuditLog {
|
|||||||
Ok(entry)
|
Ok(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lê as entradas da trilha, opcionalmente só as `limit` mais recentes.
|
/// Reads entries from the audit log, optionally returning only the last `limit` entries.
|
||||||
pub fn read_entries(path: impl AsRef<Path>, limit: Option<usize>) -> Result<Vec<AuditEntry>> {
|
pub fn read_entries(path: impl AsRef<Path>, limit: Option<usize>) -> Result<Vec<AuditEntry>> {
|
||||||
let file = OpenOptions::new().read(true).open(path)?;
|
let file = OpenOptions::new().read(true).open(path)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
@@ -105,8 +105,8 @@ impl AuditLog {
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verifica a integridade de toda a cadeia; retorna o índice da
|
/// Verifies the integrity of the entire hash chain; returns the index of
|
||||||
/// primeira entrada inválida, se houver.
|
/// the first invalid entry if tampered.
|
||||||
pub fn verify(path: impl AsRef<Path>) -> Result<Option<usize>> {
|
pub fn verify(path: impl AsRef<Path>) -> Result<Option<usize>> {
|
||||||
let file = OpenOptions::new().read(true).open(path)?;
|
let file = OpenOptions::new().read(true).open(path)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
//! Captura crua de quadros 802.11 via `AF_PACKET`, sem depender de libpcap.
|
//! Raw 802.11 frame capture via Linux `AF_PACKET` sockets without requiring `libpcap`.
|
||||||
|
//!
|
||||||
|
//! The interface must already be in monitor mode (see [`crate::netlink`]).
|
||||||
|
//! Uses the same kernel packet socket interface that `libpcap` uses on Linux
|
||||||
|
//! without adding an external system library dependency.
|
||||||
//!
|
//!
|
||||||
//! A interface já precisa estar em modo monitor (ver [`crate::netlink`]).
|
|
||||||
//! Isso só funciona em Linux — é exatamente o que o libpcap faz por baixo
|
|
||||||
//! quando usado em modo monitor, então reimplementar aqui é pouco código e
|
|
||||||
//! remove uma dependência de sistema.
|
|
||||||
|
|
||||||
#![cfg(target_os = "linux")]
|
#![cfg(target_os = "linux")]
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@ pub struct RawCapture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl RawCapture {
|
impl RawCapture {
|
||||||
/// Abre um socket raw ligado à interface `iface` (já em modo monitor).
|
/// Opens a raw `AF_PACKET` socket bound to `iface` (must already be in monitor mode).
|
||||||
/// Exige `CAP_NET_RAW` (tipicamente root).
|
/// Requires `CAP_NET_RAW` (typically root).
|
||||||
pub fn open(iface: &str) -> Result<Self> {
|
pub fn open(iface: &str) -> Result<Self> {
|
||||||
let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, ETH_P_ALL as i32) };
|
let fd = unsafe { libc::socket(libc::AF_PACKET, libc::SOCK_RAW, ETH_P_ALL as i32) };
|
||||||
if fd < 0 {
|
if fd < 0 {
|
||||||
@@ -57,8 +57,8 @@ impl RawCapture {
|
|||||||
Ok(Self { fd })
|
Ok(Self { fd })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Define um timeout de recepção, para o loop de captura poder checar
|
/// Sets `SO_RCVTIMEO` so the capture loop can periodically check its stop flag
|
||||||
/// periodicamente um sinal de parada sem travar para sempre em `recv`.
|
/// without blocking indefinitely in `recv`.
|
||||||
pub fn set_read_timeout(&self, timeout: Duration) -> Result<()> {
|
pub fn set_read_timeout(&self, timeout: Duration) -> Result<()> {
|
||||||
let tv = libc::timeval {
|
let tv = libc::timeval {
|
||||||
tv_sec: timeout.as_secs() as libc::time_t,
|
tv_sec: timeout.as_secs() as libc::time_t,
|
||||||
@@ -79,7 +79,7 @@ impl RawCapture {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lê um quadro. Retorna `Ok(None)` em timeout (ver `set_read_timeout`).
|
/// Reads a single frame. Returns `Ok(None)` on receive timeout (see `set_read_timeout`).
|
||||||
pub fn recv(&self, buf: &mut [u8]) -> Result<Option<usize>> {
|
pub fn recv(&self, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||||
let n = unsafe { libc::recv(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) };
|
let n = unsafe { libc::recv(self.fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len(), 0) };
|
||||||
if n < 0 {
|
if n < 0 {
|
||||||
@@ -114,8 +114,8 @@ fn if_index(name: &str) -> Result<i32> {
|
|||||||
Ok(idx as i32)
|
Ok(idx as i32)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Garante, de forma best-effort, que o processo tem privilégio para abrir
|
/// Best-effort check that the process has privileges to open raw packet sockets.
|
||||||
/// sockets raw — útil para dar um erro claro antes de tentar o resto.
|
///
|
||||||
pub fn check_capabilities() -> bool {
|
pub fn check_capabilities() -> bool {
|
||||||
let euid = unsafe { libc::geteuid() };
|
let euid = unsafe { libc::geteuid() };
|
||||||
euid == 0
|
euid == 0
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
//! Decriptação CCMP (WPA2-AES) de quadros de dados, usando a TK já derivada
|
//! WPA2 CCMP (AES-CTR) data frame decryption using the Temporal Key (TK) derived
|
||||||
//! de um handshake quebrado.
|
//! from a cracked 4-way handshake.
|
||||||
|
//!
|
||||||
|
//! Used passively to inspect cleartext upper-layer protocols (DHCP, mDNS, ARP, HTTP)
|
||||||
|
//! of a station whose own 4-way handshake was captured and cracked, allowing
|
||||||
|
//! hostname/device identification. Because each association derives an ephemeral
|
||||||
|
//! PTK, the TK is scoped to a specific `(BSSID, station)` pair.
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//!
|
||||||
|
//! Implements AES-CTR payload decryption (skipping 8-byte CBC-MAC MIC verification
|
||||||
|
//! since we only extract passive identity hints from known-key traffic).
|
||||||
|
//!
|
||||||
|
//!
|
||||||
//!
|
//!
|
||||||
//! Isso não serve pra forjar nem reinjetar nada — só pra enxergar o conteúdo
|
|
||||||
//! em claro (DHCP, mDNS) de um cliente cuja senha da rede já foi recuperada
|
|
||||||
//! por dicionário, e assim extrair hostname/fabricante de verdade em vez de
|
|
||||||
//! só o MAC. A chave é par (BSSID, estação): só decifra tráfego do cliente
|
|
||||||
//! cujo próprio handshake foi capturado e quebrado, nunca de terceiros —
|
|
||||||
//! cada associação tem sua PTK efêmera, então a senha da rede sozinha não
|
|
||||||
//! abre o tráfego de quem a gente não viu entrar.
|
|
||||||
//!
|
//!
|
||||||
//! Implementa só o modo CTR (a parte que decifra o conteúdo). O MIC
|
|
||||||
//! (CBC-MAC) não é conferido: a intenção aqui é mostrar o payload de um
|
|
||||||
//! tráfego que a gente já pode decifrar porque a senha é conhecida, não
|
|
||||||
//! provar integridade de um quadro adulterado — e implementar o AAD/CBC-MAC
|
|
||||||
//! certinho exigiria replicar o mascaramento do cabeçalho 802.11 inteiro
|
|
||||||
//! sem ganho prático pro que isso serve aqui.
|
|
||||||
|
|
||||||
use aes::cipher::{BlockEncrypt, KeyInit};
|
use aes::cipher::{BlockEncrypt, KeyInit};
|
||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
|
|
||||||
/// Decifra o corpo de um quadro de dados CCMP, a partir do cabeçalho CCMP
|
/// Decrypts a CCMP-protected data frame payload starting at the 8-byte CCMP header
|
||||||
/// (8 bytes) em diante — ou seja, `body` é `frame.body[frame.header_len..]`.
|
/// (`frame.body[frame.header_len..]`).
|
||||||
/// `transmitter` é o endereço 2 (A2) do quadro — o rádio que o enviou, não o
|
/// `transmitter` is Address 2 (`A2`), and `qos_tid` is the QoS TID (`0` for non-QoS).
|
||||||
/// BSSID/estação lógicos. `qos_tid` é o TID do QoS Control (0 se não-QoS).
|
///
|
||||||
|
///
|
||||||
|
/// Returns the decrypted LLC/SNAP + payload (excluding the trailing 8-byte MIC).
|
||||||
///
|
///
|
||||||
/// Retorna o LLC/SNAP + payload em claro (o mesmo formato que um quadro não
|
|
||||||
/// protegido teria), sem os 8 bytes de MIC cifrado no final.
|
|
||||||
pub fn decrypt_data(
|
pub fn decrypt_data(
|
||||||
tk: &[u8; 16],
|
tk: &[u8; 16],
|
||||||
transmitter: &[u8; 6],
|
transmitter: &[u8; 6],
|
||||||
@@ -40,8 +40,8 @@ pub fn decrypt_data(
|
|||||||
let pn0 = body[0];
|
let pn0 = body[0];
|
||||||
let pn1 = body[1];
|
let pn1 = body[1];
|
||||||
let key_id_octet = body[3];
|
let key_id_octet = body[3];
|
||||||
// ExtIV (bit 5) precisa estar ligado — senão não é um cabeçalho CCMP de
|
// ExtIV (bit 5) must be set for a valid 8-byte CCMP IV header.
|
||||||
// verdade (podia ser WEP de 4 bytes por coincidência de layout).
|
//
|
||||||
if key_id_octet & 0x20 == 0 {
|
if key_id_octet & 0x20 == 0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -53,8 +53,8 @@ pub fn decrypt_data(
|
|||||||
let ciphertext_and_mic = &body[HEADER_LEN..];
|
let ciphertext_and_mic = &body[HEADER_LEN..];
|
||||||
let ciphertext = &ciphertext_and_mic[..ciphertext_and_mic.len() - MIC_LEN];
|
let ciphertext = &ciphertext_and_mic[..ciphertext_and_mic.len() - MIC_LEN];
|
||||||
|
|
||||||
// Nonce CCMP (13 bytes): flags/prioridade (1) || A2 (6) || PN, do byte
|
// 13-byte CCMP nonce: `flags/priority (1) || A2 (6) || PN5..PN0 (6)`.
|
||||||
// mais significativo (PN5) ao menos significativo (PN0).
|
//
|
||||||
let mut nonce = [0u8; 13];
|
let mut nonce = [0u8; 13];
|
||||||
nonce[0] = qos_tid & 0x0f;
|
nonce[0] = qos_tid & 0x0f;
|
||||||
nonce[1..7].copy_from_slice(transmitter);
|
nonce[1..7].copy_from_slice(transmitter);
|
||||||
@@ -67,10 +67,10 @@ pub fn decrypt_data(
|
|||||||
|
|
||||||
let cipher = Aes128::new(tk.into());
|
let cipher = Aes128::new(tk.into());
|
||||||
let mut out = Vec::with_capacity(ciphertext.len());
|
let mut out = Vec::with_capacity(ciphertext.len());
|
||||||
let mut counter: u16 = 1; // o bloco de contador 0 cifra só o MIC, não dado.
|
let mut counter: u16 = 1; // counter block 0 encrypts the MIC; payload blocks start at counter 1.
|
||||||
for chunk in ciphertext.chunks(16) {
|
for chunk in ciphertext.chunks(16) {
|
||||||
let mut block = [0u8; 16];
|
let mut block = [0u8; 16];
|
||||||
block[0] = 0x01; // flag octet do CCMP counter mode (L=1, sem AAD aqui)
|
block[0] = 0x01; // CCMP counter mode flags octet (`L = 1`)
|
||||||
block[1..14].copy_from_slice(&nonce);
|
block[1..14].copy_from_slice(&nonce);
|
||||||
block[14..16].copy_from_slice(&counter.to_be_bytes());
|
block[14..16].copy_from_slice(&counter.to_be_bytes());
|
||||||
let mut keystream = block.into();
|
let mut keystream = block.into();
|
||||||
@@ -87,9 +87,9 @@ pub fn decrypt_data(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// AES-128 do FIPS-197 (vetor de referência oficial do NIST): confirma
|
/// NIST FIPS-197 Appendix B AES-128 test vector.
|
||||||
/// que a cifra de bloco usada embaixo está correta antes de confiar no
|
///
|
||||||
/// contador CTR por cima dela.
|
///
|
||||||
#[test]
|
#[test]
|
||||||
fn aes128_matches_fips197_vector() {
|
fn aes128_matches_fips197_vector() {
|
||||||
let key: [u8; 16] = [
|
let key: [u8; 16] = [
|
||||||
@@ -110,19 +110,19 @@ mod tests {
|
|||||||
assert_eq!(block.as_slice(), &expected);
|
assert_eq!(block.as_slice(), &expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Round-trip de auto-consistência: decifrar um texto que a própria
|
/// CTR symmetry round-trip: encrypting and decrypting with the same CCMP
|
||||||
/// função "cifrou" (mesmo algoritmo, usado nos dois sentidos — CTR é
|
/// keystream recovers the original LLC/SNAP payload.
|
||||||
/// simétrico) tem que devolver o original. Isso pega bug de indexação
|
///
|
||||||
/// no nonce/contador mesmo sem um vetor de teste oficial do CCMP à mão.
|
///
|
||||||
#[test]
|
#[test]
|
||||||
fn ctr_round_trip_recovers_plaintext() {
|
fn ctr_round_trip_recovers_plaintext() {
|
||||||
let tk = [0x42u8; 16];
|
let tk = [0x42u8; 16];
|
||||||
let transmitter = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
let transmitter = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
|
||||||
let plaintext = b"AA AA 03 00 00 00 08 00 payload de teste bem maior que um bloco de 16 bytes para cobrir mais de uma rodada do contador";
|
let plaintext = b"AA AA 03 00 00 00 08 00 payload de teste bem maior que um bloco de 16 bytes para cobrir mais de uma rodada do contador";
|
||||||
|
|
||||||
// Monta um "quadro" CCMP cifrando manualmente com a mesma lógica de
|
// Build a synthetic CCMP payload using the symmetric CTR keystream.
|
||||||
// keystream (ela é simétrica: XOR com o keystream cifra e decifra).
|
//
|
||||||
let header = [0x11u8, 0x22, 0x00, 0x20, 0x33, 0x44, 0x55, 0x66]; // ExtIV ligado
|
let header = [0x11u8, 0x22, 0x00, 0x20, 0x33, 0x44, 0x55, 0x66]; // ExtIV bit set
|
||||||
let mut nonce = [0u8; 13];
|
let mut nonce = [0u8; 13];
|
||||||
nonce[0] = 3; // TID
|
nonce[0] = 3; // TID
|
||||||
nonce[1..7].copy_from_slice(&transmitter);
|
nonce[1..7].copy_from_slice(&transmitter);
|
||||||
@@ -147,7 +147,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
counter = counter.wrapping_add(1);
|
counter = counter.wrapping_add(1);
|
||||||
}
|
}
|
||||||
ciphertext.extend_from_slice(&[0u8; 8]); // MIC fictício, ignorado na decriptação
|
ciphertext.extend_from_slice(&[0u8; 8]); // dummy 8-byte MIC (stripped during decryption)
|
||||||
|
|
||||||
let mut body = Vec::new();
|
let mut body = Vec::new();
|
||||||
body.extend_from_slice(&header);
|
body.extend_from_slice(&header);
|
||||||
@@ -160,7 +160,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rejects_body_without_extiv_bit() {
|
fn rejects_body_without_extiv_bit() {
|
||||||
let tk = [0u8; 16];
|
let tk = [0u8; 16];
|
||||||
let body = [0u8; 20]; // key_id_octet (índice 3) sem o bit ExtIV
|
let body = [0u8; 20]; // key_id_octet (index 3) without the ExtIV bit set
|
||||||
assert!(decrypt_data(&tk, &[0; 6], 0, &body).is_none());
|
assert!(decrypt_data(&tk, &[0; 6], 0, &body).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
//! Quebra de WPA/WPA2-PSK por dicionário, em Rust puro.
|
//! Pure-Rust WPA/WPA2-PSK dictionary and mask cracker.
|
||||||
//!
|
//!
|
||||||
//! Duas provas são aceitas, como no aircrack-ng/hashcat:
|
//! Supports both verification modes used by `aircrack-ng` and `hashcat`:
|
||||||
//!
|
//!
|
||||||
//! * **EAPOL (4-way handshake):** deriva a PMK com PBKDF2-HMAC-SHA1
|
//! * **EAPOL (4-way handshake):** derives the PMK via PBKDF2-HMAC-SHA1
|
||||||
//! (4096 iterações), expande a PTK com o PRF do 802.11i e recalcula o MIC da
|
//! (4096 iterations), expands the PTK via IEEE 802.11i PRF, and verifies the
|
||||||
//! mensagem 2.
|
//! message 2 MIC.
|
||||||
//! * **PMKID:** recalcula `HMAC-SHA1(PMK, "PMK Name" || AP || STA)` direto da
|
//! * **PMKID:** computes `HMAC-SHA1(PMK, "PMK Name" || AP || STA)` directly from
|
||||||
//! mensagem 1 — não precisa do handshake completo.
|
//! message 1 without requiring a full 4-way handshake.
|
||||||
//!
|
//!
|
||||||
//! É CPU puro, sem GPU: suficiente para a v0.1.0 e para dicionários comuns,
|
//! Runs multi-threaded on the CPU; for large GPU-accelerated attacks, export
|
||||||
//! mas não substitui o hashcat em ataques de força bruta grandes.
|
//! `.hc22000` via [`crate::hashcat`].
|
||||||
|
|
||||||
use crate::dot11::parse_mac;
|
use crate::dot11::parse_mac;
|
||||||
use crate::error::{Result, SabiaError};
|
use crate::error::{Result, SabiaError};
|
||||||
@@ -33,9 +33,9 @@ type HmacMd5 = Hmac<Md5>;
|
|||||||
|
|
||||||
const WPA_ITERATIONS: u32 = 4096;
|
const WPA_ITERATIONS: u32 = 4096;
|
||||||
|
|
||||||
/// `.gz` do dicionário embutido — descomprimido só na primeira vez que é
|
/// Gzip-compressed built-in wordlist (`~39 MB` compressed -> `~101 MB` raw),
|
||||||
/// usado (ver [`default_wordlist`]). Cru são ~101MB; comprimido, ~39MB no
|
/// decompressed lazily on first use (see [`default_wordlist`]).
|
||||||
/// binário em vez de ~101MB.
|
///
|
||||||
const DEFAULT_WORDLIST_GZ: &[u8] = include_bytes!("../assets/wordlists/wpa-full.txt.gz");
|
const DEFAULT_WORDLIST_GZ: &[u8] = include_bytes!("../assets/wordlists/wpa-full.txt.gz");
|
||||||
|
|
||||||
pub const DEFAULT_WORDLIST_SOURCE: &str =
|
pub const DEFAULT_WORDLIST_SOURCE: &str =
|
||||||
@@ -49,17 +49,17 @@ static DEFAULT_WORDLIST_CACHE: LazyLock<String> = LazyLock::new(|| {
|
|||||||
out
|
out
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Lista embutida de senhas prováveis de Wi-Fi, usada quando o usuário não
|
/// Built-in WPA wordlist (9,703,831 deduplicated 8–63 char ASCII passwords
|
||||||
/// escolhe um dicionário próprio. Não depende de arquivo externo: é a lista
|
/// combining Probable-Wordlists and `rockyou.txt` ordered by probability).
|
||||||
/// mais completa que o Sabiá consegue embutir sem exigir download —
|
/// See `crates/sabia-core/assets/wordlists/LICENSE.wpa-full` for attribution.
|
||||||
/// 9.703.831 senhas (203.806 do Probable-Wordlists, ranqueadas por
|
///
|
||||||
/// probabilidade, seguidas pelo rockyou.txt inteiro) filtradas pro
|
///
|
||||||
/// comprimento válido de WPA (8–63) e pra ASCII puro (WPA-PSK exige um
|
///
|
||||||
/// octeto válido; a imensa maioria dos roteadores só aceita ASCII mesmo, e
|
///
|
||||||
/// isso evita o pouco de lixo binário que o rockyou.txt original carrega),
|
///
|
||||||
/// sem duplicatas e na ordem de prioridade: as mais prováveis primeiro,
|
///
|
||||||
/// então a quebra tende a achar a senha cedo, não só no fim da lista. Ver
|
///
|
||||||
/// `assets/wordlists/LICENSE.wpa-full` pra atribuição completa.
|
///
|
||||||
pub fn default_wordlist() -> &'static str {
|
pub fn default_wordlist() -> &'static str {
|
||||||
&DEFAULT_WORDLIST_CACHE
|
&DEFAULT_WORDLIST_CACHE
|
||||||
}
|
}
|
||||||
@@ -142,7 +142,7 @@ impl CrackTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deriva a PMK (256 bits) de uma frase-senha WPA e do SSID.
|
/// Derives the 256-bit WPA PMK from an ASCII passphrase and SSID (`PBKDF2-HMAC-SHA1`, 4096 rounds).
|
||||||
pub fn derive_pmk(passphrase: &str, ssid: &str) -> [u8; 32] {
|
pub fn derive_pmk(passphrase: &str, ssid: &str) -> [u8; 32] {
|
||||||
let dk = pbkdf2_sha1(passphrase.as_bytes(), ssid.as_bytes(), WPA_ITERATIONS, 32);
|
let dk = pbkdf2_sha1(passphrase.as_bytes(), ssid.as_bytes(), WPA_ITERATIONS, 32);
|
||||||
let mut pmk = [0u8; 32];
|
let mut pmk = [0u8; 32];
|
||||||
@@ -150,10 +150,10 @@ pub fn derive_pmk(passphrase: &str, ssid: &str) -> [u8; 32] {
|
|||||||
pmk
|
pmk
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Aceita uma frase-senha (8–63 caracteres) ou uma PMK crua em hexadecimal
|
/// Resolves either an 8–63 character WPA passphrase or a raw 64-hex-character PMK.
|
||||||
/// (64 caracteres, como no hashcat/aircrack). `pub(crate)` porque
|
/// Shared with `handshake.rs` when deriving the CCMP Temporal Key (TK) from a known password.
|
||||||
/// `handshake.rs` reusa exatamente essa regra ao derivar a TK de uma senha
|
///
|
||||||
/// já quebrada (o campo `password` aceita as duas formas, igual aqui).
|
///
|
||||||
pub(crate) fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32]> {
|
pub(crate) fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32]> {
|
||||||
if candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()) {
|
if candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()) {
|
||||||
return hex_to_32(candidate.as_bytes());
|
return hex_to_32(candidate.as_bytes());
|
||||||
@@ -170,10 +170,10 @@ pub(crate) fn pmk_from_candidate(candidate: &str, ssid: &[u8]) -> Option<[u8; 32
|
|||||||
pub fn pbkdf2_sha1(password: &[u8], salt: &[u8], iterations: u32, out_len: usize) -> Vec<u8> {
|
pub fn pbkdf2_sha1(password: &[u8], salt: &[u8], iterations: u32, out_len: usize) -> Vec<u8> {
|
||||||
let iterations = iterations.max(1);
|
let iterations = iterations.max(1);
|
||||||
let mut out = Vec::with_capacity(out_len);
|
let mut out = Vec::with_capacity(out_len);
|
||||||
// O par ipad/opad depende só da senha — monta uma vez e clona a cada
|
// Precompute the HMAC inner/outer SHA-1 states once per passphrase and clone
|
||||||
// rodada (clone é só copiar os dois blocos já prontos) em vez de montar
|
// across all 4096 PBKDF2 rounds to avoid re-hashing the key block each iteration.
|
||||||
// o HMAC do zero em cada uma das milhares de iterações. Em 4096
|
//
|
||||||
// iterações por bloco isso é a maior parte do custo fora do SHA1 em si.
|
//
|
||||||
let base = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave");
|
let base = HmacSha1::new_from_slice(password).expect("HMAC aceita qualquer chave");
|
||||||
let mut block: u32 = 1;
|
let mut block: u32 = 1;
|
||||||
while out.len() < out_len {
|
while out.len() < out_len {
|
||||||
@@ -213,7 +213,7 @@ fn prf_sha1(key: &[u8], label: &[u8], data: &[u8], out_len: usize) -> Vec<u8> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PTK de 64 bytes: KCK (16) + KEK (16) + TK (16) + 16 de reserva.
|
/// Derives the 64-byte PTK: `KCK (16) || KEK (16) || TK (16) || MIC keys (16)`.
|
||||||
pub fn compute_ptk(
|
pub fn compute_ptk(
|
||||||
pmk: &[u8; 32],
|
pmk: &[u8; 32],
|
||||||
bssid: &[u8; 6],
|
bssid: &[u8; 6],
|
||||||
@@ -314,14 +314,14 @@ fn valid_candidate(candidate: &str) -> bool {
|
|||||||
|| (candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()))
|
|| (candidate.len() == 64 && candidate.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tenta cada linha do dicionário contra o alvo, dividindo o trabalho entre
|
/// Tests wordlist candidates against `target` across all available CPU threads.
|
||||||
/// as threads disponíveis (PBKDF2-HMAC-SHA1 de 4096 iterações é CPU puro e
|
/// Invokes `progress` (~8x/s) with the number of tested candidates. When
|
||||||
/// cada candidato é independente — ganho quase linear com o número de
|
/// `apply_rules` is enabled, expands each base word via [`rules::expand`].
|
||||||
/// núcleos). `progress` recebe o número de candidatos já processados, ~8x
|
///
|
||||||
/// por segundo enquanto roda. Com `apply_rules`, cada linha vira até
|
///
|
||||||
/// [`rules::VARIANTS_PER_WORD`] candidatos (maiúscula, sufixos comuns, leet
|
///
|
||||||
/// simples) em vez de só a palavra crua — multiplica o tempo de verdade,
|
///
|
||||||
/// por isso é opt-in.
|
///
|
||||||
pub fn crack_reader<R: BufRead, F: FnMut(u64) + Send>(
|
pub fn crack_reader<R: BufRead, F: FnMut(u64) + Send>(
|
||||||
target: &CrackTarget,
|
target: &CrackTarget,
|
||||||
mut wordlist: R,
|
mut wordlist: R,
|
||||||
@@ -330,9 +330,9 @@ pub fn crack_reader<R: BufRead, F: FnMut(u64) + Send>(
|
|||||||
) -> CrackReport {
|
) -> CrackReport {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
|
|
||||||
// Carrega a lista inteira antes de começar: dá pra repartir em pedaços
|
// Load candidates upfront and partition into contiguous per-thread slices.
|
||||||
// contíguos por thread (melhor localidade de cache que round-robin) e o
|
//
|
||||||
// próprio dicionário embutido cabe de sobra em memória.
|
//
|
||||||
let mut candidates: Vec<String> = Vec::new();
|
let mut candidates: Vec<String> = Vec::new();
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
loop {
|
loop {
|
||||||
@@ -384,8 +384,8 @@ pub fn crack_reader<R: BufRead, F: FnMut(u64) + Send>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread "repórter": roda dentro do mesmo scope só pra poder
|
// Progress reporter thread scoped to borrow `progress` (`FnMut`) safely.
|
||||||
// emprestar `progress` (FnMut, chamado de um único lugar).
|
//
|
||||||
scope.spawn(|| loop {
|
scope.spawn(|| loop {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(120));
|
std::thread::sleep(std::time::Duration::from_millis(120));
|
||||||
let n = tried.load(Ordering::Relaxed);
|
let n = tried.load(Ordering::Relaxed);
|
||||||
@@ -426,7 +426,7 @@ pub fn crack_handshake<F: FnMut(u64) + Send>(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quebra usando a lista embutida.
|
/// Runs a dictionary crack using the built-in wordlist.
|
||||||
pub fn crack_handshake_default<F: FnMut(u64) + Send>(
|
pub fn crack_handshake_default<F: FnMut(u64) + Send>(
|
||||||
hs: &Handshake,
|
hs: &Handshake,
|
||||||
apply_rules: bool,
|
apply_rules: bool,
|
||||||
@@ -449,7 +449,7 @@ fn target_for(hs: &Handshake) -> Result<CrackTarget> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lê um `.pcap`, monta os handshakes e tenta quebrar o primeiro alvo útil.
|
/// Parses a `.pcap` file, assembles handshakes, and cracks the first crackable target.
|
||||||
pub fn crack_pcap<F: FnMut(u64) + Send>(
|
pub fn crack_pcap<F: FnMut(u64) + Send>(
|
||||||
pcap: impl AsRef<Path>,
|
pcap: impl AsRef<Path>,
|
||||||
wordlist: impl AsRef<Path>,
|
wordlist: impl AsRef<Path>,
|
||||||
@@ -474,11 +474,11 @@ pub fn crack_pcap<F: FnMut(u64) + Send>(
|
|||||||
crack_handshake(hs, wordlist, apply_rules, progress)
|
crack_handshake(hs, wordlist, apply_rules, progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Força bruta por máscara (estilo hashcat `-a 3`): gera e testa cada
|
/// Mask brute-force attack (hashcat `-a 3` style): generates candidates on the fly
|
||||||
/// combinação possível, sem depender de dicionário. Mesma paralelização por
|
/// by mixed-radix index across all CPU threads without materializing the keyspace in RAM.
|
||||||
/// threads do `crack_reader`, mas gerando candidatos por índice (base
|
///
|
||||||
/// mista) em vez de carregar tudo em memória — é o que permite máscaras com
|
///
|
||||||
/// milhões/bilhões de combinações sem estourar RAM.
|
///
|
||||||
pub fn crack_mask<F: FnMut(u64) + Send>(
|
pub fn crack_mask<F: FnMut(u64) + Send>(
|
||||||
target: &CrackTarget,
|
target: &CrackTarget,
|
||||||
mask: &Mask,
|
mask: &Mask,
|
||||||
@@ -546,7 +546,7 @@ pub fn crack_mask<F: FnMut(u64) + Send>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Quebra por máscara a partir de um handshake já montado.
|
/// Runs a mask brute-force attack against an assembled `Handshake`.
|
||||||
pub fn crack_handshake_mask<F: FnMut(u64) + Send>(
|
pub fn crack_handshake_mask<F: FnMut(u64) + Send>(
|
||||||
hs: &Handshake,
|
hs: &Handshake,
|
||||||
mask: &Mask,
|
mask: &Mask,
|
||||||
@@ -562,8 +562,8 @@ mod tests {
|
|||||||
use crate::testutil::{handshake_frames, synthetic_handshake};
|
use crate::testutil::{handshake_frames, synthetic_handshake};
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
|
||||||
/// Mede só o tempo de descomprimir o dicionário embutido (gzip, ~39MB -> ~101MB),
|
/// Benchmarks one-time decompression of the built-in gzip wordlist (`~39 MB -> ~101 MB`).
|
||||||
/// que acontece uma vez, na primeira chamada a `default_wordlist()`.
|
///
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn bench_default_wordlist_decompress() {
|
fn bench_default_wordlist_decompress() {
|
||||||
@@ -577,17 +577,17 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Não é teste de verdade — mede candidatos/segundo pra guiar otimização
|
/// Throughput benchmark (`cargo test --release -p sabia-core -- --ignored --nocapture bench_crack_throughput`).
|
||||||
/// de performance. `cargo test --release -p sabia-core -- --ignored
|
///
|
||||||
/// --nocapture bench_crack_throughput`.
|
///
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
fn bench_crack_throughput() {
|
fn bench_crack_throughput() {
|
||||||
let (m1, m2) = handshake_frames("IEEE", "senha-que-nao-esta-na-lista", 2, false);
|
let (m1, m2) = handshake_frames("IEEE", "senha-que-nao-esta-na-lista", 2, false);
|
||||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||||
// Só uma fatia: a lista inteira (9,7 milhões) levaria minutos — o
|
// Benchmark a 20,000-password slice to measure candidates/sec quickly.
|
||||||
// objetivo aqui é medir candidatos/s, não o tempo total.
|
//
|
||||||
let sample: String = default_wordlist()
|
let sample: String = default_wordlist()
|
||||||
.lines()
|
.lines()
|
||||||
.take(500_000)
|
.take(500_000)
|
||||||
@@ -677,8 +677,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rules_find_a_mutated_variant_not_present_verbatim() {
|
fn rules_find_a_mutated_variant_not_present_verbatim() {
|
||||||
// A senha de verdade é "wifi12345" — não está na lista, mas é um
|
// True password `"wifi12345"` is derived from `"wifi"` via `rules::expand`.
|
||||||
// sufixo numérico de "wifi" que `rules::expand` gera.
|
//
|
||||||
let (m1, m2) = handshake_frames("IEEE", "wifi12345", 2, false);
|
let (m1, m2) = handshake_frames("IEEE", "wifi12345", 2, false);
|
||||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||||
@@ -692,10 +692,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn mask_crack_finds_a_numeric_password() {
|
fn mask_crack_finds_a_numeric_password() {
|
||||||
// Máscara minúscula de propósito (só 10 combinações): cada candidato
|
// Small 10-candidate mask (`wifi123?d`) to keep unit tests fast.
|
||||||
// é um PBKDF2 de 4096 iterações, pesado até em paralelo — um brute
|
//
|
||||||
// force de verdade (ex.: ?d×8, 100 milhões) é coisa pra uso real,
|
//
|
||||||
// não pra um teste unitário que roda em `cargo test` toda hora.
|
//
|
||||||
let (m1, m2) = handshake_frames("IEEE", "wifiabc7", 2, false);
|
let (m1, m2) = handshake_frames("IEEE", "wifiabc7", 2, false);
|
||||||
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
let hs = synthetic_handshake("IEEE", "00:11:22:33:44:55", "66:77:88:99:aa:bb", &m1, &m2);
|
||||||
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
let target = CrackTarget::from_handshake(&hs).expect("alvo");
|
||||||
|
|||||||