Compare commits
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)
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Documentation-only or local agent state changes do not trigger CI jobs.
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
|
||||||
|
pull_request:
|
||||||
|
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ gitea.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
RUSTFLAGS: -D warnings
|
||||||
|
# The runner persists /opt/hostedtoolcache across jobs: rustup toolchains,
|
||||||
|
# cargo registry, npm/apt caches, and target directories live there so nothing
|
||||||
|
# is re-downloaded or rebuilt from scratch on every run. Each job uses its own
|
||||||
|
# target directory so differing RUSTFLAGS do not invalidate another job's cache.
|
||||||
|
CARGO_HOME: /opt/hostedtoolcache/cargo
|
||||||
|
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
||||||
|
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Format and clippy
|
||||||
|
runs-on: linux-arm64
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-lint
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: UI translation coverage
|
||||||
|
run: node scripts/check-ui-i18n.mjs
|
||||||
|
- run: cargo fmt --all -- --check
|
||||||
|
- run: cargo clippy --workspace --all-targets
|
||||||
|
|
||||||
|
# Linux-only runner (ARM64).
|
||||||
|
test:
|
||||||
|
name: Tests (Linux)
|
||||||
|
runs-on: linux-arm64
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- run: cargo test --workspace
|
||||||
|
|
||||||
|
audit:
|
||||||
|
name: Dependency audit
|
||||||
|
runs-on: linux-arm64
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
# rustsec/audit-check depends on GitHub's checks API. cargo-audit itself
|
||||||
|
# needs nothing but the advisory database, and installing it once into the
|
||||||
|
# persistent CARGO_HOME makes every later run skip compilation.
|
||||||
|
- name: cargo audit
|
||||||
|
run: |
|
||||||
|
command -v cargo-audit >/dev/null || cargo install cargo-audit --locked
|
||||||
|
cargo audit
|
||||||
|
|
||||||
|
gui:
|
||||||
|
name: Check desktop app (Linux)
|
||||||
|
runs-on: linux-arm64
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-gui
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: Tauri system dependencies
|
||||||
|
timeout-minutes: 25
|
||||||
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Generate icons
|
||||||
|
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
||||||
|
- run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: ""
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
# Triggered when a v* tag is pushed: creates the Gitea Release entry with notes
|
||||||
|
# from CHANGELOG.md and uploads the compiled CLI (scripts/package-cli.sh) with
|
||||||
|
# a .sha256 checksum alongside each archive, plus the desktop bundles (Linux
|
||||||
|
# aarch64, Linux x86_64, and Windows via cargo-xwin).
|
||||||
|
#
|
||||||
|
# Updater signing (optional): SABIA_TAURI_SIGNING_PRIVATE_KEY and
|
||||||
|
# SABIA_TAURI_SIGNING_PRIVATE_KEY_PASSWORD, with unprefixed names as fallback.
|
||||||
|
# Without a key, the build succeeds without generating .sig files.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
# Rust toolchains, cargo registry, and caches live on the persistent runner volume.
|
||||||
|
CARGO_HOME: /opt/hostedtoolcache/cargo
|
||||||
|
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
||||||
|
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
|
||||||
|
XWIN_CACHE_DIR: /opt/hostedtoolcache/xwin-cache
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Create release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Release
|
||||||
|
env:
|
||||||
|
TAG: ${{ gitea.ref_name }}
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: |
|
||||||
|
# Re-running the workflow does not recreate an existing release.
|
||||||
|
if curl -fsS -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" >/dev/null 2>&1; then
|
||||||
|
echo "release $TAG already exists"; exit 0
|
||||||
|
fi
|
||||||
|
scripts/release-notes.sh "$TAG" > notes.md
|
||||||
|
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
||||||
|
jq -n --arg tag "$TAG" --arg name "Sabiá ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
|
||||||
|
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
||||||
|
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' -d @release.json >/dev/null
|
||||||
|
|
||||||
|
cli:
|
||||||
|
name: CLI (${{ matrix.platform }})
|
||||||
|
needs: release
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-cli-${{ matrix.platform }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { platform: linux-aarch64, target: aarch64-unknown-linux-gnu, apt: "", runner: linux-arm64 }
|
||||||
|
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu", runner: linux-amd64 }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
- name: Platform cross-linker
|
||||||
|
if: ${{ matrix.apt != '' && matrix.runner != 'linux-amd64' }}
|
||||||
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/cli-x86_64 ${{ matrix.apt }}
|
||||||
|
- name: Build and package
|
||||||
|
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
|
||||||
|
- name: Upload to release
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: scripts/release-upload.sh "${{ gitea.ref_name }}" $(cat packages.txt)
|
||||||
|
|
||||||
|
gui:
|
||||||
|
name: Desktop app (${{ matrix.platform }})
|
||||||
|
needs: release
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
env:
|
||||||
|
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-release-gui-${{ matrix.platform }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.SABIA_TAURI_SIGNING_PRIVATE_KEY || secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.SABIA_TAURI_SIGNING_PRIVATE_KEY_PASSWORD || secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { platform: linux-aarch64, runner: linux-arm64 }
|
||||||
|
- { platform: linux-x86_64, runner: linux-amd64 }
|
||||||
|
- { platform: windows, runner: linux-amd64 }
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: actions/setup-node@v7
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
- name: Tauri dependencies (Linux aarch64)
|
||||||
|
if: matrix.platform == 'linux-aarch64'
|
||||||
|
timeout-minutes: 25
|
||||||
|
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
- name: Tauri dependencies (Linux x86_64)
|
||||||
|
if: matrix.platform == 'linux-x86_64'
|
||||||
|
timeout-minutes: 25
|
||||||
|
run: |
|
||||||
|
if [ "$(uname -m)" = aarch64 ]; then
|
||||||
|
dpkg --add-architecture amd64
|
||||||
|
. /etc/os-release
|
||||||
|
if [ "${ID:-}" = ubuntu ]; then
|
||||||
|
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
|
||||||
|
grep -q '^Architectures:' /etc/apt/sources.list.d/ubuntu.sources || sed -i '/^Types: deb/a Architectures: arm64' /etc/apt/sources.list.d/ubuntu.sources
|
||||||
|
printf 'Types: deb\nURIs: http://archive.ubuntu.com/ubuntu/\nSuites: %s %s-updates %s-security\nComponents: main universe\nArchitectures: amd64\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.sources
|
||||||
|
elif [ -f /etc/apt/sources.list ]; then
|
||||||
|
sed -i 's/^deb http/deb [arch=arm64] http/' /etc/apt/sources.list
|
||||||
|
printf 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-updates main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-security main universe\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.list
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-amd64 gcc-x86-64-linux-gnu g++-x86-64-linux-gnu pkg-config patchelf file squashfs-tools qemu-user libglib2.0-bin libwebkit2gtk-4.1-dev:amd64 libayatana-appindicator3-dev:amd64 librsvg2-dev:amd64
|
||||||
|
else
|
||||||
|
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||||
|
fi
|
||||||
|
- name: Cross-compilation tools (Windows)
|
||||||
|
if: matrix.platform == 'windows'
|
||||||
|
run: |
|
||||||
|
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-win nsis lld llvm clang
|
||||||
|
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
|
||||||
|
- name: Build and package
|
||||||
|
run: scripts/build-gui.sh "${{ matrix.platform }}"
|
||||||
|
- name: Upload to release
|
||||||
|
env:
|
||||||
|
TOKEN: ${{ gitea.token }}
|
||||||
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||||
|
run: scripts/release-upload.sh "${{ gitea.ref_name }}" dist-gui/*
|
||||||
@@ -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,83 +0,0 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Mudança só de documentação não precisa de 6 jobs em 3 sistemas.
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**']
|
|
||||||
pull_request:
|
|
||||||
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**']
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ci-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
CARGO_TERM_COLOR: always
|
|
||||||
RUSTFLAGS: -D warnings
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Formatação e clippy
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
with:
|
|
||||||
components: rustfmt, clippy
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
- name: Cobertura da tradução da interface
|
|
||||||
run: node scripts/check-ui-i18n.mjs
|
|
||||||
- run: cargo fmt --all -- --check
|
|
||||||
- run: cargo clippy --workspace --all-targets
|
|
||||||
|
|
||||||
test:
|
|
||||||
name: Testes (${{ matrix.os }})
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-24.04, windows-latest, macos-latest]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
- run: cargo test --workspace
|
|
||||||
|
|
||||||
audit:
|
|
||||||
name: Auditoria de dependências
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: rustsec/audit-check@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
gui:
|
|
||||||
name: Compila o app desktop (Linux)
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v7
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: apps/gui/src-tauri
|
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
- name: Cache dos pacotes do Tauri (.deb)
|
|
||||||
uses: actions/cache@v6
|
|
||||||
with:
|
|
||||||
path: ~/apt-cache
|
|
||||||
key: apt-tauri-${{ runner.os }}-${{ runner.arch }}-${{ env.ImageVersion }}-${{ hashFiles('scripts/apt-install.sh') }}
|
|
||||||
- name: Dependências do Tauri
|
|
||||||
timeout-minutes: 25
|
|
||||||
run: scripts/apt-install.sh ~/apt-cache libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
|
||||||
- name: Gera os ícones
|
|
||||||
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
|
||||||
- run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: ""
|
|
||||||
@@ -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/
|
||||||
@@ -18,3 +26,17 @@ apps/gui/src-tauri/target
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
GEMINI.md
|
GEMINI.md
|
||||||
|
|
||||||
|
# Local environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Local scratch files
|
||||||
|
*.eml
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Packages generated by scripts/package-cli.sh and scripts/build-gui.sh
|
||||||
|
/guara-v*
|
||||||
|
/sabia-v*
|
||||||
|
/dist-gui
|
||||||
@@ -4,6 +4,52 @@
|
|||||||
|
|
||||||
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.5] - 2026-10-31
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **One-click desktop update.** The update button actually downloads, verifies (SHA-256) and installs the new version now, instead of only changing its own label. Works for all three desktop packages: AppImage (atomic self-replace), `.deb` (`dpkg -i`, no new password prompt — the app is already running as root by the time you click it), and the Windows installer (silent install). On Linux, the app restarts itself automatically; on Windows, the installer runs silently but the app does not reopen on its own yet — start it again once the install finishes.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **The repository moved to the Ciberfauna organization** (`ciberfauna/sabia`, was `lucasrguerra/sabia`). **Important notice for existing installations:** versions up to 1.0.4 have the old location baked into their update check; they will not find this release automatically. Download 1.0.5 once manually from the new Releases page, and future update checks will work through the new location.
|
||||||
|
|
||||||
|
## [1.0.3] - 2026-10-30
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `deauth_directional` had no `#[cfg(target_os = "linux")]` of its own; every
|
||||||
|
non-Linux build failed with a duplicate-definition error and unresolved
|
||||||
|
Linux-only imports. `v1.0.1` and `v1.0.2` never got a working Windows
|
||||||
|
desktop app for this reason, and `v1.0.2`'s Linux package was mislabeled
|
||||||
|
`1.0.1` (the version in `Cargo.toml` was not bumped). Use `v1.0.3`.
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-10-30
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- The desktop app build on CI looked for its bundle under `target/`, but the
|
||||||
|
runner's Cargo target directory is elsewhere and persists between runs; the
|
||||||
|
Linux and Windows jobs failed before publishing anything. `v1.0.1`'s desktop
|
||||||
|
app was never published for this reason — use `v1.0.2`.
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-10-30
|
||||||
|
|
||||||
|
The project moved from GitHub to Gitea, and the update check followed it.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **Updates now come from Gitea.** `sabia update` and the desktop app's "Update now"
|
||||||
|
read the latest release from `git.lucasrguerra.dev.br` instead of the GitHub API.
|
||||||
|
Versions up to 1.0.0 still look at GitHub, which is no longer reachable, so they
|
||||||
|
cannot find this release: download 1.0.1 once, and later updates are automatic.
|
||||||
|
- Release packages: the CLI for Linux x86_64 and aarch64 (built by the Gitea CI when
|
||||||
|
a `v*` tag is pushed, with a `.sha256` next to each package), and the desktop app
|
||||||
|
for Linux (`.AppImage`, `.deb`) and Windows (NSIS `.exe` installer, cross-compiled
|
||||||
|
from Linux by `scripts/build-gui.sh`). **There is no Windows CLI and no macOS build
|
||||||
|
any more**: the CLI is Linux-only, and macOS needs Apple's SDK and code signing,
|
||||||
|
which this project's CI cannot provide.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- `sabia update` refuses to run from the desktop app's executable (`sabia-gui`).
|
||||||
|
The release package contains the CLI, and replacing the app with it would have
|
||||||
|
broken the app; the app now points to the downloads page instead.
|
||||||
|
|
||||||
## [1.0.0] - 2026-10-24
|
## [1.0.0] - 2026-10-24
|
||||||
|
|
||||||
First stable release: real-hardware validation across the whole attack surface, a much more complete cracking pipeline, two new attacks (WPS, Evil Twin), a redesigned report, and several reliability fixes found only by testing against live Wi-Fi networks.
|
First stable release: real-hardware validation across the whole attack surface, a much more complete cracking pipeline, two new attacks (WPS, Evil Twin), a redesigned report, and several reliability fixes found only by testing against live Wi-Fi networks.
|
||||||
|
|||||||
@@ -4,6 +4,52 @@
|
|||||||
|
|
||||||
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.5] - 2026-10-31
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- **Atualização do app desktop em um clique.** O botão de atualizar agora baixa, confere (SHA-256) e instala a versão nova de verdade, em vez de só mudar o próprio texto. Funciona nos três pacotes do desktop: AppImage (substituição atômica), `.deb` (`dpkg -i`, sem pedir senha de novo — o app já roda como root quando você clica), e o instalador do Windows (instalação silenciosa). No Linux o app reabre sozinho; no Windows o instalador roda silencioso mas o app ainda não reabre por conta própria — abra de novo depois que a instalação terminar.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **O repositório se mudou pra organização Ciberfauna** (`ciberfauna/sabia`, era `lucasrguerra/sabia`). **Aviso importante pra instalações existentes:** versões até a 1.0.4 têm o endereço antigo fixo na checagem de atualização e não vão encontrar esta versão sozinhas. Baixe a 1.0.5 manualmente uma vez na nova página de Releases, e as próximas verificações voltam a funcionar pelo novo endereço.
|
||||||
|
|
||||||
|
## [1.0.3] - 2026-10-30
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
- O `deauth_directional` não tinha o próprio `#[cfg(target_os = "linux")]`;
|
||||||
|
todo build fora do Linux falhava com uma definição duplicada e imports só de
|
||||||
|
Linux fora de escopo. Por isso a `v1.0.1` e a `v1.0.2` nunca tiveram um app
|
||||||
|
desktop funcional para Windows, e o pacote Linux da `v1.0.2` saiu rotulado
|
||||||
|
`1.0.1` (a versão no `Cargo.toml` não tinha sido atualizada). Use a `v1.0.3`.
|
||||||
|
|
||||||
|
## [1.0.2] - 2026-10-30
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
- O build do app desktop no CI procurava o pacote em `target/`, mas o
|
||||||
|
diretório do Cargo no runner é outro e persiste entre execuções; os jobs de
|
||||||
|
Linux e Windows falhavam antes de publicar qualquer coisa. Por isso o app
|
||||||
|
desktop da `v1.0.1` nunca foi publicado — use a `v1.0.2`.
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-10-30
|
||||||
|
|
||||||
|
O projeto saiu do GitHub para o Gitea, e a verificação de atualização foi junto.
|
||||||
|
|
||||||
|
### Alterado
|
||||||
|
- **As atualizações agora vêm do Gitea.** O `sabia update` e o "Atualizar agora" do app
|
||||||
|
desktop leem a última release em `git.lucasrguerra.dev.br`, e não mais a API do
|
||||||
|
GitHub. As versões até a 1.0.0 ainda olham o GitHub, que não responde mais, e por isso
|
||||||
|
não enxergam esta release: baixe a 1.0.1 uma vez, e as próximas atualizações são automáticas.
|
||||||
|
- Pacotes da release: o CLI para Linux x86_64 e aarch64 (compilado pelo CI do Gitea
|
||||||
|
quando uma tag `v*` é enviada, com um `.sha256` ao lado de cada pacote) e o app desktop
|
||||||
|
para Linux (`.AppImage`, `.deb`) e Windows (instalador NSIS `.exe`, compilado a partir
|
||||||
|
do Linux pelo `scripts/build-gui.sh`). **Não há mais CLI para Windows nem build para
|
||||||
|
macOS**: o CLI é só para Linux, e o macOS exige o SDK e a assinatura da Apple, que o CI
|
||||||
|
deste projeto não tem.
|
||||||
|
|
||||||
|
### Corrigido
|
||||||
|
- O `sabia update` se recusa a rodar a partir do executável do app desktop (`sabia-gui`).
|
||||||
|
O pacote da release contém o CLI, e trocar o app por ele o quebraria; o app passa a
|
||||||
|
apontar para a página de downloads.
|
||||||
|
|
||||||
## [1.0.0] - 2026-10-24
|
## [1.0.0] - 2026-10-24
|
||||||
|
|
||||||
Primeira versão estável: validação em hardware real por toda a superfície de ataque, um pipeline de quebra bem mais completo, dois ataques novos (WPS, Evil Twin), relatório redesenhado e várias correções de confiabilidade que só apareceram testando contra redes Wi-Fi de verdade.
|
Primeira versão estável: validação em hardware real por toda a superfície de ataque, um pipeline de quebra bem mais completo, dois ataques novos (WPS, Evil Twin), relatório redesenhado e várias correções de confiabilidade que só apareceram testando contra redes Wi-Fi de verdade.
|
||||||
|
|||||||
@@ -7,19 +7,27 @@ Thanks for helping! Golden rules:
|
|||||||
1. **Authorized auditing only.** Every new feature is for testing your own networks or networks you're authorized to test. Sensitive operations (deauthentication) require explicit confirmation and are recorded in the audit trail. PRs that hide activity from the trail or remove the confirmations are rejected.
|
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, Windows and macOS; run these before pushing — almost everything that fails CI shows up here in seconds. Documentation-only changes (`*.md`, `docs/`) are light on CI.
|
CI runs on Linux; run the check before pushing: almost everything that fails CI shows up here in seconds. Documentation-only changes (`*.md`, `docs/`) skip CI.
|
||||||
|
|
||||||
Live capture exists only on Linux (`AF_PACKET` + netlink); on Windows and macOS those paths compile but aren't exercised. Parsing and cracking tests run on all three, because they're offline.
|
Live capture exists only on Linux (`AF_PACKET` + netlink); the code behind Windows and macOS paths still compiles and is covered by `cargo check`.
|
||||||
|
|
||||||
|
## Releases and Versioning
|
||||||
|
|
||||||
|
Versions strictly follow **Semantic Versioning 2.0.0** (`MAJOR.MINOR.PATCH`), synchronized across `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml`, and `apps/gui/src-tauri/tauri.conf.json` (enforced by `scripts/check.sh`).
|
||||||
|
|
||||||
|
Pushing a `v*` tag makes the Gitea CI create the release (notes come from `CHANGELOG.md`, so update it first) and attach the Linux CLI (x86_64 and aarch64) and the desktop app for Linux (x86_64 and aarch64) and Windows.
|
||||||
|
|
||||||
|
There is no macOS build: it needs Apple's SDK and code signing, which a Linux runner cannot provide.
|
||||||
|
|
||||||
## Where to work
|
## Where to work
|
||||||
|
|
||||||
@@ -38,6 +46,10 @@ The source text of the interface is **Portuguese**; English comes from `apps/gui
|
|||||||
|
|
||||||
Keep the pt/en pair: every `*.md` and `docs/*` file has a Portuguese version (`*.pt-BR.md`, `docs/pt-BR/`) and an English one.
|
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,19 +7,27 @@ Obrigado por ajudar! Regras de ouro:
|
|||||||
1. **Só auditoria autorizada.** Todo recurso novo é para testar redes próprias ou com autorização. Operações sensíveis (desautenticação) exigem confirmação explícita e ficam na trilha de auditoria. PRs que escondem atividade da trilha ou removem as confirmações são recusados.
|
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 em Linux, Windows e macOS; rode esses comandos antes do push — quase tudo que faz o CI falhar aparece aqui em segundos. Mudanças só de documentação (`*.md`, `docs/`) são leves no CI.
|
O CI roda em Linux; rode a verificação antes do push: quase tudo que faz o CI falhar aparece aqui em segundos. Mudanças só de documentação (`*.md`, `docs/`) pulam o CI.
|
||||||
|
|
||||||
A captura ao vivo só existe no Linux (`AF_PACKET` + netlink); em Windows e macOS esses caminhos são compilados mas não exercitados. Testes de parsing e de quebra rodam nas três plataformas, porque são offline.
|
A captura ao vivo só existe no Linux (`AF_PACKET` + netlink); o código dos caminhos de Windows e macOS continua compilando e é coberto pelo `cargo check`.
|
||||||
|
|
||||||
|
## Releases e Versionamento
|
||||||
|
|
||||||
|
As versões seguem estritamente o **Versionamento Semântico 2.0.0** (`MAJOR.MINOR.PATCH`), sincronizadas entre `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml` e `apps/gui/src-tauri/tauri.conf.json` (validado por `scripts/check.sh`).
|
||||||
|
|
||||||
|
Enviar uma tag `v*` faz o CI do Gitea criar a release (as notas vêm do `CHANGELOG.md`, então atualize-o antes) e anexar o CLI para Linux (x86_64 e aarch64) e o app desktop para Linux (x86_64 e aarch64) e Windows.
|
||||||
|
|
||||||
|
Não há build para macOS: ele exige o SDK e a assinatura da Apple, que um runner Linux não tem.
|
||||||
|
|
||||||
## Onde mexer
|
## Onde mexer
|
||||||
|
|
||||||
@@ -38,6 +46,10 @@ O texto-fonte da interface é em **português**; o inglês vem de `apps/gui/ui/i
|
|||||||
|
|
||||||
Mantenha o par pt/en: todo arquivo `*.md` e `docs/*` tem a versão em português (`*.pt-BR.md`, `docs/pt-BR/`) e em inglês.
|
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.0"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
@@ -451,7 +451,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-core"
|
name = "sabia-core"
|
||||||
version = "1.0.0"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ resolver = "2"
|
|||||||
members = ["crates/sabia-core", "crates/sabia-cli"]
|
members = ["crates/sabia-core", "crates/sabia-cli"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.0.0"
|
version = "1.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/lucasrguerra/sabia"
|
repository = "https://git.lucasrguerra.dev.br/ciberfauna/sabia"
|
||||||
authors = ["Sabiá contributors"]
|
authors = ["Sabiá contributors"]
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
@@ -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, Linux and macOS · authorized use.</p>
|
<p align="center">Free and open Wi-Fi auditing.<br>Terminal and desktop app · live capture on Linux, offline analysis on Windows too · authorized use.</p>
|
||||||
|
|
||||||
<p align="center"><a href="docs/cli.md">Terminal</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/architecture.md">Architecture</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
|
<p align="center"><a href="docs/cli.md">Terminal</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/architecture.md">Architecture</a> · <a href="docs/support.md">Support</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
|
||||||
|
|
||||||
<p align="center"><b>English</b> · <a href="README.pt-BR.md">Português</a></p>
|
<p align="center"><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:
|
||||||
@@ -37,17 +39,18 @@ Download the package for your system from **[Releases](../../releases)**:
|
|||||||
|
|
||||||
| | CLI | Desktop app |
|
| | CLI | Desktop app |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Windows | `sabia-*-windows-x86_64.zip` | `.msi` / `.exe` installer |
|
| Windows | — | `.exe` installer |
|
||||||
| macOS | `sabia-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` |
|
|
||||||
| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
||||||
|
|
||||||
Each file has a `.sha256` next to it. Build from source: `cargo build --release -p sabia-cli` (Rust 1.80+).
|
The CLI is Linux-only; the desktop app ships for Linux and Windows. Each CLI
|
||||||
|
package has a `.sha256` next to it. Build from source: `cargo build --release
|
||||||
|
-p sabia-cli` (Rust 1.80+, Linux).
|
||||||
|
|
||||||
**Updates:** the desktop app checks GitHub on startup; when a new version exists, a button offers **Update now**: it downloads the right binary for your platform, verifies the **SHA-256** and swaps the running executable — no manual download. The check is a single request to the releases API and sends nothing of yours.
|
**Updates:** the desktop app checks Gitea on startup; when a new version exists, a button offers **Update now**. The CLI updates the same way (`sabia update`); it downloads the right binary for your platform, verifies the **SHA-256** and swaps the running executable — no manual download. The check is a single request to the releases API and sends nothing of yours.
|
||||||
|
|
||||||
> The binaries are **not signed yet**: Windows SmartScreen and macOS Gatekeeper will warn. Check the SHA-256.
|
> The binaries are **not signed yet**: Windows SmartScreen will warn. Check the SHA-256.
|
||||||
|
|
||||||
**Capture and monitor mode require Linux and root** (`CAP_NET_ADMIN`/`CAP_NET_RAW`): raw capture uses `AF_PACKET` and interface control uses netlink. **Offline `.pcap` analysis and cracking** also run on Windows and macOS. The desktop app elevates to administrator on its own at startup.
|
**Capture and monitor mode require Linux and root** — the only platform the CLI ships for (`CAP_NET_ADMIN`/`CAP_NET_RAW`): raw capture uses `AF_PACKET` and interface control uses netlink. **Offline `.pcap` analysis and cracking** also run on Windows and macOS. The desktop app elevates to administrator on its own at startup.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -67,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.
|
||||||
@@ -74,11 +79,11 @@ Desktop app (Tauri) with an interface sidebar, a capture panel with an animated
|
|||||||
|
|
||||||
## Known limits
|
## Known limits
|
||||||
|
|
||||||
- **Capture is Linux-only**, with a card that supports **monitor mode** (and injection, for `deauth`). On Windows and macOS Sabiá only does `.pcap` analysis and offline cracking.
|
- **Capture is Linux-only**, with a card that supports **monitor mode** (and injection, for `deauth`); the CLI is not built for other platforms. On Windows Sabiá's desktop app only does `.pcap` analysis and offline cracking.
|
||||||
- **WPA3/SAE** does not fall to handshake dictionaries — cracking covers **WPA/WPA2-PSK** (4-way and PMKID).
|
- **WPA3/SAE** does not fall to handshake dictionaries — cracking covers **WPA/WPA2-PSK** (4-way and PMKID).
|
||||||
- **5 and 6 GHz** depend on the card and the regulatory domain; some channels stay passive (no probing).
|
- **5 and 6 GHz** depend on the card and the regulatory domain; some channels stay passive (no probing).
|
||||||
- **Evil Twin's self-signed certificate:** the Enterprise variant's TLS certificate isn't trusted by clients that validate the chain (common on updated corporate/MDM devices) — that's the correct defense working, not a bug.
|
- **Evil Twin's self-signed certificate:** the Enterprise variant's TLS certificate isn't trusted by clients that validate the chain (common on updated corporate/MDM devices) — that's the correct defense working, not a bug.
|
||||||
- **Windows and macOS:** the code compiles and is tested in CI, but live capture does not run on those platforms.
|
- **Windows:** the code compiles and is tested in CI, but live capture does not run there; the desktop app does offline analysis only. There is no macOS build: Tauri needs the macOS SDK and code signing, both tied to Apple hardware and an Apple Developer account, which this project's CI (a single Linux runner) does not have.
|
||||||
|
|
||||||
## Where it's going
|
## Where it's going
|
||||||
|
|
||||||
@@ -88,6 +93,7 @@ Summary of the [ROADMAP](ROADMAP.md):
|
|||||||
|---|---|
|
|---|---|
|
||||||
| **Done** | Monitor mode via netlink (with NetworkManager recovery), 2.4/5/6 GHz scanning, 4-way and PMKID capture, CCMP decryption, dictionary/rules/mask cracking (WPA/WPA2-PSK), `.hc22000` export, `.pcap` read/write, authorized `deauth`/PMF/PMKID, WPS attack, Evil Twin (PSK + Enterprise), channel scoring and findings, paginated audit report, hash-chained audit trail, `doctor`, CLI + desktop app, one-click update |
|
| **Done** | Monitor mode via netlink (with NetworkManager recovery), 2.4/5/6 GHz scanning, 4-way and PMKID capture, CCMP decryption, dictionary/rules/mask cracking (WPA/WPA2-PSK), `.hc22000` export, `.pcap` read/write, authorized `deauth`/PMF/PMKID, WPS attack, Evil Twin (PSK + Enterprise), channel scoring and findings, paginated audit report, hash-chained audit trail, `doctor`, CLI + desktop app, one-click update |
|
||||||
| **Next** | GPU acceleration for cracking, capture on Windows (Npcap) |
|
| **Next** | GPU acceleration for cracking, capture on Windows (Npcap) |
|
||||||
|
| **Not planned** | A macOS build — see [Known limits](#known-limits) |
|
||||||
| **Later** | WPA3/SAE downgrade, validation on varied hardware, signed release binaries |
|
| **Later** | WPA3/SAE downgrade, validation on varied hardware, signed release binaries |
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
@@ -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 em Windows, Linux e macOS · uso autorizado.</p>
|
<p align="center">Auditoria de Wi-Fi livre e gratuita.<br>Terminal e interface gráfica · captura no Linux, análise offline também em Windows · uso autorizado.</p>
|
||||||
|
|
||||||
<p align="center"><a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
|
<p align="center"><a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="docs/pt-BR/suporte.md">Suporte</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
|
||||||
|
|
||||||
<p align="center"><a href="README.md">English</a> · <b>Português</b></p>
|
<p align="center"><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`:
|
||||||
@@ -37,17 +39,18 @@ Baixe o pacote do seu sistema em **[Releases](../../releases)**:
|
|||||||
|
|
||||||
| | CLI | App gráfico |
|
| | CLI | App gráfico |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Windows | `sabia-*-windows-x86_64.zip` | instalador `.msi` / `.exe` |
|
| Windows | — | instalador `.exe` |
|
||||||
| macOS | `sabia-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` |
|
|
||||||
| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
||||||
|
|
||||||
Cada arquivo tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p sabia-cli` (Rust 1.80+).
|
O CLI é só para Linux; o app desktop sai para Linux e Windows. Cada pacote do
|
||||||
|
CLI tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p
|
||||||
|
sabia-cli` (Rust 1.80+, Linux).
|
||||||
|
|
||||||
**Atualizações:** o app desktop consulta o GitHub ao abrir; havendo versão nova, um botão oferece **Atualizar agora**: ele baixa o binário certo para a plataforma, confere o **SHA-256** e troca o executável em uso — você não precisa baixar nada na mão. A consulta é uma única requisição à API de releases e nada seu é enviado.
|
**Atualizações:** o app desktop consulta o Gitea ao abrir; havendo versão nova, um botão oferece **Atualizar agora**. O CLI atualiza do mesmo jeito (`sabia update`); ele baixa o binário certo para a plataforma, confere o **SHA-256** e troca o executável em uso — você não precisa baixar nada na mão. A consulta é uma única requisição à API de releases e nada seu é enviado.
|
||||||
|
|
||||||
> Os binários ainda **não são assinados**: o SmartScreen (Windows) e o Gatekeeper (macOS) vão avisar. Confira o SHA-256.
|
> Os binários ainda **não são assinados**: o SmartScreen do Windows vai avisar. Confira o SHA-256.
|
||||||
|
|
||||||
**Captura e modo monitor exigem Linux e privilégio de root** (`CAP_NET_ADMIN`/`CAP_NET_RAW`): a captura crua usa `AF_PACKET` e o controle de interface usa netlink. A **análise de `.pcap` e a quebra offline** rodam também em Windows e macOS. O app desktop pede elevação de administrador sozinho ao abrir.
|
**Captura e modo monitor exigem Linux e privilégio de root** — a única plataforma para a qual o CLI é compilado (`CAP_NET_ADMIN`/`CAP_NET_RAW`): a captura crua usa `AF_PACKET` e o controle de interface usa netlink. A **análise de `.pcap` e a quebra offline** rodam também em Windows e macOS. O app desktop pede elevação de administrador sozinho ao abrir.
|
||||||
|
|
||||||
## Começando
|
## Começando
|
||||||
|
|
||||||
@@ -67,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.
|
||||||
@@ -74,11 +79,11 @@ App desktop (Tauri) com sidebar de interfaces, painel de captura com linha de si
|
|||||||
|
|
||||||
## Limites conhecidos
|
## Limites conhecidos
|
||||||
|
|
||||||
- **Captura só no Linux**, com placa que suporte **modo monitor** (e injeção, para `deauth`). Em Windows e macOS o Sabiá faz só análise de `.pcap` e quebra offline.
|
- **Captura só no Linux**, com placa que suporte **modo monitor** (e injeção, para `deauth`); o CLI não é compilado para outras plataformas. Em Windows o app desktop do Sabiá faz só análise de `.pcap` e quebra offline.
|
||||||
- **WPA3/SAE** não cai por dicionário de handshake — a quebra cobre **WPA/WPA2-PSK** (4-way e PMKID).
|
- **WPA3/SAE** não cai por dicionário de handshake — a quebra cobre **WPA/WPA2-PSK** (4-way e PMKID).
|
||||||
- **5 e 6 GHz** dependem da placa e do _regulatory domain_; alguns canais ficam passivos (sem _probe_).
|
- **5 e 6 GHz** dependem da placa e do _regulatory domain_; alguns canais ficam passivos (sem _probe_).
|
||||||
- **Certificado autoassinado do Evil Twin Enterprise:** clientes que validam a cadeia de certificado da rede (comum em perfil corporativo/MDM atualizado) não confiam nele — é a defesa correta funcionando, não um bug.
|
- **Certificado autoassinado do Evil Twin Enterprise:** clientes que validam a cadeia de certificado da rede (comum em perfil corporativo/MDM atualizado) não confiam nele — é a defesa correta funcionando, não um bug.
|
||||||
- **Windows e macOS:** o código compila e é testado em CI, mas a captura ao vivo não roda nessas plataformas.
|
- **Windows:** o código compila e é testado em CI, mas a captura ao vivo não roda lá; o app desktop faz só análise offline. Não há build para macOS: o Tauri precisa do SDK do macOS e de assinatura, os dois presos a hardware Apple e a uma conta Apple Developer, que o CI deste projeto (um único runner Linux) não tem.
|
||||||
|
|
||||||
## Para onde vai
|
## Para onde vai
|
||||||
|
|
||||||
@@ -88,6 +93,7 @@ Resumo do [ROADMAP](ROADMAP.pt-BR.md):
|
|||||||
|---|---|
|
|---|---|
|
||||||
| **Feito** | Modo monitor por netlink (com recuperação do NetworkManager), varredura 2.4/5/6 GHz, captura de 4-way e PMKID, decriptação CCMP, quebra por dicionário/regras/máscara (WPA/WPA2-PSK), exportação `.hc22000`, escrita e leitura de `.pcap`, `deauth`/PMF/PMKID autorizados, ataque WPS, Evil Twin (PSK + Enterprise), pontuação de canais e achados, relatório de auditoria paginado, trilha de auditoria por hash, `doctor`, CLI + app desktop, atualização em um clique |
|
| **Feito** | Modo monitor por netlink (com recuperação do NetworkManager), varredura 2.4/5/6 GHz, captura de 4-way e PMKID, decriptação CCMP, quebra por dicionário/regras/máscara (WPA/WPA2-PSK), exportação `.hc22000`, escrita e leitura de `.pcap`, `deauth`/PMF/PMKID autorizados, ataque WPS, Evil Twin (PSK + Enterprise), pontuação de canais e achados, relatório de auditoria paginado, trilha de auditoria por hash, `doctor`, CLI + app desktop, atualização em um clique |
|
||||||
| **Próximo** | Aceleração por GPU na quebra, captura em Windows (Npcap) |
|
| **Próximo** | Aceleração por GPU na quebra, captura em Windows (Npcap) |
|
||||||
|
| **Fora de plano** | Um build para macOS — veja [Limites conhecidos](#limites-conhecidos) |
|
||||||
| **Depois** | WPA3/SAE _downgrade_, validação em hardware variado, binários de release assinados |
|
| **Depois** | WPA3/SAE _downgrade_, validação em hardware variado, binários de release assinados |
|
||||||
|
|
||||||
## Desenvolvimento
|
## Desenvolvimento
|
||||||
|
|||||||
@@ -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.0"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"flate2",
|
"flate2",
|
||||||
@@ -2555,7 +2555,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sabia-gui"
|
name = "sabia-gui"
|
||||||
version = "1.0.0"
|
version = "1.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"sabia-core",
|
"sabia-core",
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# 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.0"
|
version = "1.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
repository = "https://github.com/lucasrguerra/sabia"
|
authors = ["Sabiá contributors"]
|
||||||
|
repository = "https://git.lucasrguerra.dev.br/ciberfauna/sabia"
|
||||||
rust-version = "1.80"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
|||||||
@@ -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};
|
||||||
@@ -502,13 +502,24 @@ fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, Str
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
|
fn update_apply(app: tauri::AppHandle, state: tauri::State<'_, State>) -> Result<String, String> {
|
||||||
state.0.update_apply().map_err(|e| e.to_string())
|
use tauri::Emitter;
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
let exe = elevated_exe().unwrap_or_else(|_| std::env::current_exe().unwrap_or_default());
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
let exe = std::env::current_exe().unwrap_or_default();
|
||||||
|
|
||||||
|
state
|
||||||
|
.0
|
||||||
|
.update_apply_desktop(&exe, move |phase| {
|
||||||
|
let _ = app.emit("update-progress", serde_json::json!({ "phase": phase }));
|
||||||
|
})
|
||||||
|
.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 +532,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 +621,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 +639,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 +651,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 +660,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.0",
|
"version": "1.0.5",
|
||||||
"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 || '');
|
||||||
@@ -267,6 +267,8 @@ async function tauriApi(path, opts = {}) {
|
|||||||
return await invoke('export_all_hc22000', { iface: q.get('iface'), out: q.get('out') });
|
return await invoke('export_all_hc22000', { iface: q.get('iface'), out: q.get('out') });
|
||||||
case 'GET /api/update/check':
|
case 'GET /api/update/check':
|
||||||
return await invoke('update_check');
|
return await invoke('update_check');
|
||||||
|
case 'POST /api/update/apply':
|
||||||
|
return await invoke('update_apply');
|
||||||
default:
|
default:
|
||||||
return { error: 'rota desconhecida' };
|
return { error: 'rota desconhecida' };
|
||||||
}
|
}
|
||||||
@@ -280,8 +282,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 +354,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;
|
||||||
}
|
}
|
||||||
@@ -371,12 +373,54 @@ els.btnScan.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let updateStatus = null;
|
||||||
|
let updateInProgress = false;
|
||||||
|
|
||||||
els.btnUpdate.addEventListener('click', async () => {
|
els.btnUpdate.addEventListener('click', async () => {
|
||||||
|
if (updateInProgress) return;
|
||||||
|
if (updateStatus?.update_available) {
|
||||||
|
updateInProgress = true;
|
||||||
|
els.btnUpdate.disabled = true;
|
||||||
|
els.btnUpdate.textContent = 'baixando…';
|
||||||
|
|
||||||
|
let unlisten = null;
|
||||||
|
if (tauri?.event?.listen) {
|
||||||
|
try {
|
||||||
|
unlisten = await tauri.event.listen('update-progress', (event) => {
|
||||||
|
const phase = event.payload?.phase;
|
||||||
|
if (phase === 'downloading') els.btnUpdate.textContent = 'baixando…';
|
||||||
|
else if (phase === 'verifying') els.btnUpdate.textContent = 'verificando…';
|
||||||
|
else if (phase === 'installing') els.btnUpdate.textContent = 'instalando…';
|
||||||
|
else if (phase === 'restarting') els.btnUpdate.textContent = 'reiniciando…';
|
||||||
|
});
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api('/api/update/apply', { method: 'POST' });
|
||||||
|
els.btnUpdate.textContent = 'já atualizado';
|
||||||
|
} catch (err) {
|
||||||
|
updateInProgress = false;
|
||||||
|
els.btnUpdate.disabled = false;
|
||||||
|
els.btnUpdate.textContent = `atualizar para ${updateStatus.latest_version}`;
|
||||||
|
alert('Falha na atualização: ' + (err?.message || err));
|
||||||
|
} finally {
|
||||||
|
if (unlisten) unlisten();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
els.btnUpdate.textContent = 'checando…';
|
els.btnUpdate.textContent = 'checando…';
|
||||||
|
try {
|
||||||
const status = await api('/api/update/check');
|
const status = await api('/api/update/check');
|
||||||
els.btnUpdate.textContent = status.update_available
|
updateStatus = status;
|
||||||
|
els.btnUpdate.textContent = status?.update_available
|
||||||
? `atualizar para ${status.latest_version}`
|
? `atualizar para ${status.latest_version}`
|
||||||
: 'já atualizado';
|
: 'já atualizado';
|
||||||
|
} catch (err) {
|
||||||
|
els.btnUpdate.textContent = 'checar atualização';
|
||||||
|
alert('Não foi possível verificar atualizações: ' + (err?.message || err));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const SEC_RANK = {
|
const SEC_RANK = {
|
||||||
@@ -398,7 +442,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 +458,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 +504,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 +579,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 +676,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 +796,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 +823,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 +847,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 +1100,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 +1121,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 +1565,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 +1714,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 +1731,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 +1745,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 +2413,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 +2428,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 +2447,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',
|
||||||
@@ -12,6 +12,12 @@ export const EN = {
|
|||||||
'checando…': 'checking…',
|
'checando…': 'checking…',
|
||||||
'atualizar para {n}': 'update to {n}',
|
'atualizar para {n}': 'update to {n}',
|
||||||
'já atualizado': 'already up to date',
|
'já atualizado': 'already up to date',
|
||||||
|
'baixando…': 'downloading…',
|
||||||
|
'verificando…': 'verifying…',
|
||||||
|
'instalando…': 'installing…',
|
||||||
|
'reiniciando…': 'restarting…',
|
||||||
|
'Falha na atualização:': 'Update failed:',
|
||||||
|
'Não foi possível verificar atualizações:': 'Could not check for updates:',
|
||||||
|
|
||||||
// topbar
|
// topbar
|
||||||
'Painel de captura': 'Capture dashboard',
|
'Painel de captura': 'Capture dashboard',
|
||||||
@@ -19,7 +25,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 +33,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 +78,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 +88,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 +139,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 +346,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 +364,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 +484,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 +531,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 +553,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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||