Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e19a8e5b | ||
|
|
bf059fd0bc | ||
|
|
a904723f96 | ||
|
|
2dfbbfc64a | ||
|
|
659701b2d4 | ||
|
|
30a086a808 | ||
|
|
833abc6308 | ||
|
|
3fc3febff3 | ||
|
|
827ef0c2c0 | ||
|
|
c7638a2f4e | ||
|
|
d885d92643 | ||
|
|
0e31d28342 | ||
|
|
9bcdd6c139 | ||
|
|
f62e7fd0a1 | ||
|
|
62ce8b60d5 | ||
|
|
d3ddf86317 | ||
|
|
6e0cd7cf10 | ||
|
|
a3ca73e610 | ||
|
|
7b39c67830 | ||
|
|
91aacba939 | ||
|
|
f83654506e | ||
|
|
5477b8fb03 |
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: ubuntu-latest
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-lint
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
- name: UI translation coverage
|
||||
run: node scripts/check-ui-i18n.mjs
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy --workspace --all-targets
|
||||
|
||||
# Linux-only runner (ARM64).
|
||||
test:
|
||||
name: Tests (Linux)
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-test
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo test --workspace
|
||||
|
||||
audit:
|
||||
name: Dependency audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# rustsec/audit-check depends on GitHub's checks API. cargo-audit itself
|
||||
# needs nothing but the advisory database, and installing it once into the
|
||||
# persistent CARGO_HOME makes every later run skip compilation.
|
||||
- name: cargo audit
|
||||
run: |
|
||||
command -v cargo-audit >/dev/null || cargo install cargo-audit --locked
|
||||
cargo audit
|
||||
|
||||
gui:
|
||||
name: Check desktop app (Linux)
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/sabia-gui
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Tauri system dependencies
|
||||
timeout-minutes: 25
|
||||
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
- name: Generate icons
|
||||
run: npx --yes @tauri-apps/cli@2 icon apps/gui/src-tauri/icons/source.png -o apps/gui/src-tauri/icons
|
||||
- run: cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
|
||||
env:
|
||||
RUSTFLAGS: ""
|
||||
@@ -0,0 +1,129 @@
|
||||
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: ubuntu-latest
|
||||
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: "" }
|
||||
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu" }
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Platform cross-linker
|
||||
if: ${{ matrix.apt != '' }}
|
||||
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: ubuntu-latest
|
||||
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:
|
||||
platform: [linux-aarch64, linux-x86_64, windows]
|
||||
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
|
||||
|
||||
# Sabiá local captures and audit log
|
||||
*.pcap
|
||||
*.cap
|
||||
.sabia-audit.log
|
||||
node_modules/
|
||||
apps/gui/src-tauri/target
|
||||
|
||||
# Configuração e memória local de agentes de IA — não faz parte do
|
||||
# produto, fica só na máquina de desenvolvimento.
|
||||
# Logo copied to the UI directory by build.rs
|
||||
/apps/gui/ui/logo.png
|
||||
|
||||
# Local AI agent configuration and state — not part of the product
|
||||
.claude/
|
||||
.wolf/
|
||||
.opencode/
|
||||
@@ -18,3 +26,17 @@ apps/gui/src-tauri/target
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
GEMINI.md
|
||||
|
||||
# Local environment files
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Local scratch files
|
||||
*.eml
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Packages generated by scripts/package-cli.sh and scripts/build-gui.sh
|
||||
/guara-v*
|
||||
/sabia-v*
|
||||
/dist-gui
|
||||
@@ -4,6 +4,111 @@
|
||||
|
||||
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
||||
|
||||
## [1.0.3] - 2026-10-30
|
||||
|
||||
### Fixed
|
||||
- `deauth_directional` had no `#[cfg(target_os = "linux")]` of its own; every
|
||||
non-Linux build failed with a duplicate-definition error and unresolved
|
||||
Linux-only imports. `v1.0.1` and `v1.0.2` never got a working Windows
|
||||
desktop app for this reason, and `v1.0.2`'s Linux package was mislabeled
|
||||
`1.0.1` (the version in `Cargo.toml` was not bumped). Use `v1.0.3`.
|
||||
|
||||
## [1.0.2] - 2026-10-30
|
||||
|
||||
### Fixed
|
||||
- The desktop app build on CI looked for its bundle under `target/`, but the
|
||||
runner's Cargo target directory is elsewhere and persists between runs; the
|
||||
Linux and Windows jobs failed before publishing anything. `v1.0.1`'s desktop
|
||||
app was never published for this reason — use `v1.0.2`.
|
||||
|
||||
## [1.0.1] - 2026-10-30
|
||||
|
||||
The project moved from GitHub to Gitea, and the update check followed it.
|
||||
|
||||
### Changed
|
||||
- **Updates now come from Gitea.** `sabia update` and the desktop app's "Update now"
|
||||
read the latest release from `git.lucasrguerra.dev.br` instead of the GitHub API.
|
||||
Versions up to 1.0.0 still look at GitHub, which is no longer reachable, so they
|
||||
cannot find this release: download 1.0.1 once, and later updates are automatic.
|
||||
- Release packages: the CLI for Linux x86_64 and aarch64 (built by the Gitea CI when
|
||||
a `v*` tag is pushed, with a `.sha256` next to each package), and the desktop app
|
||||
for Linux (`.AppImage`, `.deb`) and Windows (NSIS `.exe` installer, cross-compiled
|
||||
from Linux by `scripts/build-gui.sh`). **There is no Windows CLI and no macOS build
|
||||
any more**: the CLI is Linux-only, and macOS needs Apple's SDK and code signing,
|
||||
which this project's CI cannot provide.
|
||||
|
||||
### Fixed
|
||||
- `sabia update` refuses to run from the desktop app's executable (`sabia-gui`).
|
||||
The release package contains the CLI, and replacing the app with it would have
|
||||
broken the app; the app now points to the downloads page instead.
|
||||
|
||||
## [1.0.0] - 2026-10-24
|
||||
|
||||
First stable release: real-hardware validation across the whole attack surface, a much more complete cracking pipeline, two new attacks (WPS, Evil Twin), a redesigned report, and several reliability fixes found only by testing against live Wi-Fi networks.
|
||||
|
||||
### Added
|
||||
- **Cracking:** default dictionary replaced by a 9.7M-password list (rockyou +
|
||||
Probable-Wordlists, deduplicated, gzip-embedded); hashcat-style **mutation
|
||||
rules** (uppercase, numeric/symbol suffixes, simple leet); **mask brute
|
||||
force** (`?d?l?u?s?a` syntax); **`.hc22000` export** (single or batch) to
|
||||
hand a handshake to hashcat/John.
|
||||
- **WPS attack:** PIN/Pixie Dust via a bundled `reaver` (extracted at
|
||||
runtime), or `bully` when installed on the system.
|
||||
- **Evil Twin:** clones an already-seen AP (open SSID) on a second interface
|
||||
with a captive portal, for WPA/WPA2-PSK — the submitted password is only
|
||||
accepted after being verified against the real captured handshake, never
|
||||
"accepts anything". An **Enterprise (802.1X/EAP)** variant captures
|
||||
credentials automatically via `hostapd-wpe`, no portal or human action
|
||||
needed, since the credential travels in the EAP handshake itself.
|
||||
- **CCMP traffic decryption** for a client once its handshake's password is
|
||||
known (Temporal Key derived from the cracked PMK).
|
||||
- **Channel-interference scoring** per band and **security findings** (open
|
||||
network, missing PMF, WPS exposed, possible evil twin by duplicate SSID).
|
||||
- **Redesigned audit report:** full-page cover with embedded logo, table of
|
||||
contents, running header/footer via `@page` margin boxes (current
|
||||
section, page numbers), KPI and findings cards with severity colors —
|
||||
still paginated by Paged.js in the browser, printable to PDF.
|
||||
- `ScanSnapshot.warning`: tells you when a capture has been running for a
|
||||
while without receiving a single frame — a card that accepts turning
|
||||
monitor mode on without actually delivering capture (common with
|
||||
built-in laptop Wi-Fi) now says so, instead of silently "listening".
|
||||
|
||||
### Fixed
|
||||
- **Handshake capture never completed on real networks:** `eapol.rs` read
|
||||
`key_info`/`replay_counter`/`key_data_len` as little-endian; those
|
||||
802.1X fields are big-endian. Deauth worked, EAPOL frames were seen, but
|
||||
`HandshakeTracker` silently discarded every one — 0 handshakes no matter
|
||||
how much traffic was generated. The project's own synthetic test frames
|
||||
made the same mistake symmetrically, so round-trip tests never caught
|
||||
it; verified live against a real AP after the fix.
|
||||
- **Monitor mode fighting NetworkManager:** `set_monitor()` now releases
|
||||
the interface from NetworkManager before switching modes and returns it
|
||||
afterward — without this, NM reasserted its own connection within
|
||||
seconds and capture died with "is in managed mode".
|
||||
- **Evil Twin left the system half-configured if the process died mid-run**
|
||||
(Ctrl+C, `kill -9`, power loss): both interfaces stuck `managed=no`
|
||||
forever, Wi-Fi gone from the system panel. A state file plus a
|
||||
self-healing pass on every engine start now undoes this automatically;
|
||||
`sabia ui` also handles SIGINT gracefully.
|
||||
- **Repeated deauth/disassoc frames were dropped as duplicates** after the
|
||||
first send — the same Sequence Control was reused on every injection;
|
||||
now increments per frame.
|
||||
- **Network list reordered under the cursor** during capture, as signal-based
|
||||
sorting reacted to every 1-2 dBm fluctuation; order is now stable
|
||||
(arrival order, signal rounded to 4 dBm buckets).
|
||||
- **"Force handshake" failed if capture had been stopped:** it now resumes
|
||||
capture automatically and waits for the target to reappear with a known
|
||||
channel before starting the job, instead of locking onto whatever
|
||||
channel the scanner happened to be hopping through.
|
||||
- **Attacks tab layout left large empty gaps** when two cards of very
|
||||
different heights shared a grid row; switched to a masonry-style column
|
||||
layout.
|
||||
|
||||
### Changed
|
||||
- `crack_reader`/`crack_mask` parallelize across available cores and reuse
|
||||
the HMAC base per iteration instead of reconstructing it — roughly 640x
|
||||
faster in release builds versus the original debug/single-thread path.
|
||||
|
||||
## [0.1.0] - 2026-10-23
|
||||
|
||||
First release: CLI, desktop app and shared core.
|
||||
|
||||
@@ -4,6 +4,117 @@
|
||||
|
||||
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
||||
|
||||
## [1.0.3] - 2026-10-30
|
||||
|
||||
### Corrigido
|
||||
- O `deauth_directional` não tinha o próprio `#[cfg(target_os = "linux")]`;
|
||||
todo build fora do Linux falhava com uma definição duplicada e imports só de
|
||||
Linux fora de escopo. Por isso a `v1.0.1` e a `v1.0.2` nunca tiveram um app
|
||||
desktop funcional para Windows, e o pacote Linux da `v1.0.2` saiu rotulado
|
||||
`1.0.1` (a versão no `Cargo.toml` não tinha sido atualizada). Use a `v1.0.3`.
|
||||
|
||||
## [1.0.2] - 2026-10-30
|
||||
|
||||
### Corrigido
|
||||
- O build do app desktop no CI procurava o pacote em `target/`, mas o
|
||||
diretório do Cargo no runner é outro e persiste entre execuções; os jobs de
|
||||
Linux e Windows falhavam antes de publicar qualquer coisa. Por isso o app
|
||||
desktop da `v1.0.1` nunca foi publicado — use a `v1.0.2`.
|
||||
|
||||
## [1.0.1] - 2026-10-30
|
||||
|
||||
O projeto saiu do GitHub para o Gitea, e a verificação de atualização foi junto.
|
||||
|
||||
### Alterado
|
||||
- **As atualizações agora vêm do Gitea.** O `sabia update` e o "Atualizar agora" do app
|
||||
desktop leem a última release em `git.lucasrguerra.dev.br`, e não mais a API do
|
||||
GitHub. As versões até a 1.0.0 ainda olham o GitHub, que não responde mais, e por isso
|
||||
não enxergam esta release: baixe a 1.0.1 uma vez, e as próximas atualizações são automáticas.
|
||||
- Pacotes da release: o CLI para Linux x86_64 e aarch64 (compilado pelo CI do Gitea
|
||||
quando uma tag `v*` é enviada, com um `.sha256` ao lado de cada pacote) e o app desktop
|
||||
para Linux (`.AppImage`, `.deb`) e Windows (instalador NSIS `.exe`, compilado a partir
|
||||
do Linux pelo `scripts/build-gui.sh`). **Não há mais CLI para Windows nem build para
|
||||
macOS**: o CLI é só para Linux, e o macOS exige o SDK e a assinatura da Apple, que o CI
|
||||
deste projeto não tem.
|
||||
|
||||
### Corrigido
|
||||
- O `sabia update` se recusa a rodar a partir do executável do app desktop (`sabia-gui`).
|
||||
O pacote da release contém o CLI, e trocar o app por ele o quebraria; o app passa a
|
||||
apontar para a página de downloads.
|
||||
|
||||
## [1.0.0] - 2026-10-24
|
||||
|
||||
Primeira versão estável: validação em hardware real por toda a superfície de ataque, um pipeline de quebra bem mais completo, dois ataques novos (WPS, Evil Twin), relatório redesenhado e várias correções de confiabilidade que só apareceram testando contra redes Wi-Fi de verdade.
|
||||
|
||||
### Adicionado
|
||||
- **Quebra de senha:** dicionário padrão trocado por uma lista de 9,7 milhões
|
||||
de senhas (rockyou + Probable-Wordlists, deduplicados, comprimida e
|
||||
embutida no binário); **regras de mutação** estilo hashcat (maiúscula,
|
||||
sufixos numéricos/símbolos, leet simples); **força bruta por máscara**
|
||||
(sintaxe `?d?l?u?s?a`); **exportação `.hc22000`** (individual ou em lote)
|
||||
para levar o handshake direto ao hashcat/John.
|
||||
- **Ataque WPS:** PIN/Pixie Dust via `reaver` embutido (extraído em tempo de
|
||||
execução), ou `bully` quando instalado no sistema.
|
||||
- **Evil Twin:** clona um AP já visto na sessão (SSID aberto) numa segunda
|
||||
interface com portal cativo, para WPA/WPA2-PSK — a senha só é aceita
|
||||
depois de conferida de verdade contra o handshake real já capturado,
|
||||
nunca "aceita qualquer coisa". Uma variante **Enterprise (802.1X/EAP)**
|
||||
captura credenciais automaticamente via `hostapd-wpe`, sem portal nem
|
||||
ação humana, já que a credencial viaja no próprio handshake EAP.
|
||||
- **Decriptação de tráfego CCMP** de um cliente assim que a senha do
|
||||
handshake dele é conhecida (Temporal Key derivada da PMK quebrada).
|
||||
- **Pontuação de interferência de canais** por banda e **achados de
|
||||
segurança** (rede aberta, PMF ausente, WPS exposto, possível evil twin
|
||||
por SSID duplicado).
|
||||
- **Relatório de auditoria redesenhado:** capa em página cheia com o logo
|
||||
embutido, sumário com âncoras, cabeçalho/rodapé corridos via margin
|
||||
boxes do `@page` (seção atual, numeração de página), cards de KPI e
|
||||
achados com cor por severidade — a paginação continua feita pelo
|
||||
Paged.js no navegador, imprimível em PDF.
|
||||
- `ScanSnapshot.warning`: avisa quando uma captura está rodando há um
|
||||
tempo sem receber nenhum quadro — uma placa que aceita ativar modo
|
||||
monitor sem realmente entregar captura (comum em Wi-Fi interno de
|
||||
notebook) agora diz isso, em vez de ficar "escutando o ar" caladinha.
|
||||
|
||||
### Corrigido
|
||||
- **Captura de handshake nunca completava em rede real:** `eapol.rs` lia
|
||||
`key_info`/`replay_counter`/`key_data_len` como little-endian; esses
|
||||
campos do 802.1X são big-endian. O deauth funcionava, os quadros EAPOL
|
||||
eram vistos, mas o `HandshakeTracker` descartava todos silenciosamente —
|
||||
0 handshakes não importa quanto tráfego fosse gerado. Os próprios
|
||||
quadros sintéticos de teste do projeto cometiam o mesmo engano
|
||||
simetricamente, então os testes de ida e volta nunca pegaram isso;
|
||||
verificado ao vivo contra um AP real depois da correção.
|
||||
- **Modo monitor brigando com o NetworkManager:** `set_monitor()` agora
|
||||
solta a interface do NetworkManager antes de trocar de modo e devolve
|
||||
depois — sem isso o NM reafirmava a própria conexão em segundos e a
|
||||
captura caía com "está em modo managed".
|
||||
- **Evil Twin deixava o sistema pela metade se o processo morresse no
|
||||
meio** (Ctrl+C, `kill -9`, queda de energia): as duas interfaces ficavam
|
||||
presas em `managed=no` pra sempre, Wi-Fi sumia do painel do sistema. Um
|
||||
arquivo de estado mais uma rotina de autocura a cada início do engine
|
||||
desfazem isso sozinhos agora; o `sabia ui` também trata SIGINT
|
||||
graciosamente.
|
||||
- **Quadros repetidos de deauth/disassoc eram descartados como duplicata**
|
||||
depois do primeiro envio — o mesmo Sequence Control era reusado em toda
|
||||
injeção; agora incrementa por quadro.
|
||||
- **Lista de redes reordenava debaixo do cursor** durante a captura, porque
|
||||
a ordenação por sinal reagia a cada oscilação de 1-2 dBm; a ordem agora
|
||||
é estável (ordem de chegada, sinal arredondado em buckets de 4 dBm).
|
||||
- **"Forçar handshake" falhava se a captura tivesse sido parada:** agora
|
||||
religa a captura sozinho e espera o alvo reaparecer com canal conhecido
|
||||
antes de disparar o job, em vez de travar no canal em que o scanner por
|
||||
acaso estava saltando.
|
||||
- **Grid da aba Ataques deixava buracos vazios grandes** quando dois cards
|
||||
de alturas muito diferentes caíam na mesma linha; trocado por um layout
|
||||
de colunas tipo alvenaria.
|
||||
|
||||
### Alterado
|
||||
- `crack_reader`/`crack_mask` paralelizam entre os núcleos disponíveis e
|
||||
reaproveitam a base do HMAC por iteração em vez de reconstruí-la — cerca
|
||||
de 640x mais rápido em build release comparado ao caminho original
|
||||
debug/single-thread.
|
||||
|
||||
## [0.1.0] - 2026-10-23
|
||||
|
||||
Primeira versão: CLI, app desktop e núcleo compartilhado.
|
||||
|
||||
@@ -7,19 +7,27 @@ Thanks for helping! Golden rules:
|
||||
1. **Authorized auditing only.** Every new feature is for testing your own networks or networks you're authorized to test. Sensitive operations (deauthentication) require explicit confirmation and are recorded in the audit trail. PRs that hide activity from the trail or remove the confirmations are rejected.
|
||||
2. **Test the parsers with real data.** New readers (radiotap, 802.11, EAPOL, pcap) need tests against known frames — see `crates/sabia-core/tests/` and the helpers in `crates/sabia-core/src/testutil.rs`. No parser without a test.
|
||||
3. **Be honest about what works.** If a feature is Linux-only, or only covers WPA2-PSK, say so in `--help`, in the README and in the test.
|
||||
4. **100% English codebase and comments.** All source code, comments, scripts, CI workflows, and commit messages must be written in English, while the GUI/CLI remains bilingual (PT-BR and EN via `i18n-en.js`).
|
||||
|
||||
## Workflow
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo clippy --workspace --all-targets # as in CI (RUSTFLAGS=-D warnings)
|
||||
cargo test --workspace
|
||||
node scripts/check-ui-i18n.mjs # UI translation coverage
|
||||
scripts/install-hooks.sh # once: enables commit-msg and pre-push Git hooks
|
||||
scripts/check.sh # fast (seconds): fmt, clippy as in CI, i18n, scripts, workflows, semver
|
||||
scripts/check.sh --full # adds cargo test --workspace and cargo audit
|
||||
```
|
||||
|
||||
CI runs on Linux, Windows and macOS; run these before pushing — almost everything that fails CI shows up here in seconds. Documentation-only changes (`*.md`, `docs/`) are light on CI.
|
||||
CI runs on Linux; run the check before pushing: almost everything that fails CI shows up here in seconds. Documentation-only changes (`*.md`, `docs/`) skip CI.
|
||||
|
||||
Live capture exists only on Linux (`AF_PACKET` + netlink); on Windows and macOS those paths compile but aren't exercised. Parsing and cracking tests run on all three, because they're offline.
|
||||
Live capture exists only on Linux (`AF_PACKET` + netlink); the code behind Windows and macOS paths still compiles and is covered by `cargo check`.
|
||||
|
||||
## Releases and Versioning
|
||||
|
||||
Versions strictly follow **Semantic Versioning 2.0.0** (`MAJOR.MINOR.PATCH`), synchronized across `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml`, and `apps/gui/src-tauri/tauri.conf.json` (enforced by `scripts/check.sh`).
|
||||
|
||||
Pushing a `v*` tag makes the Gitea CI create the release (notes come from `CHANGELOG.md`, so update it first) and attach the Linux CLI (x86_64 and aarch64) and the desktop app for Linux (x86_64 and aarch64) and Windows.
|
||||
|
||||
There is no macOS build: it needs Apple's SDK and code signing, which a Linux runner cannot provide.
|
||||
|
||||
## Where to work
|
||||
|
||||
@@ -38,6 +46,10 @@ The source text of the interface is **Portuguese**; English comes from `apps/gui
|
||||
|
||||
Keep the pt/en pair: every `*.md` and `docs/*` file has a Portuguese version (`*.pt-BR.md`, `docs/pt-BR/`) and an English one.
|
||||
|
||||
## Commits
|
||||
|
||||
Commits must follow **Conventional Commits 1.0.0** in English (`feat:`, `fix:`, `chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`), e.g. `fix(eapol): validate replay counter on message 2`. One logical change per PR. Enforced automatically by `.githooks/commit-msg`.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree to license under [MIT](LICENSE-MIT) **or** [Apache-2.0](LICENSE-APACHE), like the rest of the project.
|
||||
@@ -7,19 +7,27 @@ Obrigado por ajudar! Regras de ouro:
|
||||
1. **Só auditoria autorizada.** Todo recurso novo é para testar redes próprias ou com autorização. Operações sensíveis (desautenticação) exigem confirmação explícita e ficam na trilha de auditoria. PRs que escondem atividade da trilha ou removem as confirmações são recusados.
|
||||
2. **Teste os parsers com dados reais.** Novos leitores (radiotap, 802.11, EAPOL, pcap) precisam de teste com quadros conhecidos — veja `crates/sabia-core/tests/` e os ajudantes em `crates/sabia-core/src/testutil.rs`. Nada de parser sem teste.
|
||||
3. **Seja honesto sobre o que funciona.** Se um recurso só roda no Linux, ou só cobre WPA2-PSK, diga isso no `--help`, no README e no teste.
|
||||
4. **Código, comentários e commits 100% em inglês.** Todo código-fonte, comentários, scripts, workflows de CI e mensagens de commit devem ser escritos em inglês, enquanto a interface gráfica/CLI permanece bilíngue (PT-BR e EN via `i18n-en.js`).
|
||||
|
||||
## Fluxo
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo clippy --workspace --all-targets # como no CI (RUSTFLAGS=-D warnings)
|
||||
cargo test --workspace
|
||||
node scripts/check-ui-i18n.mjs # cobertura da tradução da interface
|
||||
scripts/install-hooks.sh # uma vez: ativa os hooks commit-msg e pre-push
|
||||
scripts/check.sh # rápido (segundos): fmt, clippy como no CI, tradução, scripts, workflows, semver
|
||||
scripts/check.sh --full # inclui cargo test --workspace e cargo audit
|
||||
```
|
||||
|
||||
O CI roda em Linux, Windows e macOS; rode esses comandos antes do push — quase tudo que faz o CI falhar aparece aqui em segundos. Mudanças só de documentação (`*.md`, `docs/`) são leves no CI.
|
||||
O CI roda em Linux; rode a verificação antes do push: quase tudo que faz o CI falhar aparece aqui em segundos. Mudanças só de documentação (`*.md`, `docs/`) pulam o CI.
|
||||
|
||||
A captura ao vivo só existe no Linux (`AF_PACKET` + netlink); em Windows e macOS esses caminhos são compilados mas não exercitados. Testes de parsing e de quebra rodam nas três plataformas, porque são offline.
|
||||
A captura ao vivo só existe no Linux (`AF_PACKET` + netlink); o código dos caminhos de Windows e macOS continua compilando e é coberto pelo `cargo check`.
|
||||
|
||||
## Releases e Versionamento
|
||||
|
||||
As versões seguem estritamente o **Versionamento Semântico 2.0.0** (`MAJOR.MINOR.PATCH`), sincronizadas entre `Cargo.toml`, `apps/gui/src-tauri/Cargo.toml` e `apps/gui/src-tauri/tauri.conf.json` (validado por `scripts/check.sh`).
|
||||
|
||||
Enviar uma tag `v*` faz o CI do Gitea criar a release (as notas vêm do `CHANGELOG.md`, então atualize-o antes) e anexar o CLI para Linux (x86_64 e aarch64) e o app desktop para Linux (x86_64 e aarch64) e Windows.
|
||||
|
||||
Não há build para macOS: ele exige o SDK e a assinatura da Apple, que um runner Linux não tem.
|
||||
|
||||
## Onde mexer
|
||||
|
||||
@@ -38,6 +46,10 @@ O texto-fonte da interface é em **português**; o inglês vem de `apps/gui/ui/i
|
||||
|
||||
Mantenha o par pt/en: todo arquivo `*.md` e `docs/*` tem a versão em português (`*.pt-BR.md`, `docs/pt-BR/`) e em inglês.
|
||||
|
||||
## Commits
|
||||
|
||||
Os commits devem seguir **Conventional Commits 1.0.0** em inglês (`feat:`, `fix:`, `chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`), ex.: `fix(eapol): validate replay counter on message 2`. Uma mudança lógica por PR. Validado automaticamente por `.githooks/commit-msg`.
|
||||
|
||||
## Licença
|
||||
|
||||
Ao contribuir, você concorda em licenciar sob [MIT](LICENSE-MIT) **ou** [Apache-2.0](LICENSE-APACHE), como o resto do projeto.
|
||||
@@ -2,6 +2,23 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
@@ -88,6 +105,16 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.7"
|
||||
@@ -143,6 +170,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
@@ -209,6 +245,16 @@ version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -245,6 +291,15 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
@@ -285,6 +340,16 @@ version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
@@ -375,7 +440,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sabia-cli"
|
||||
version = "0.1.0"
|
||||
version = "1.0.3"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"ctrlc",
|
||||
@@ -386,8 +451,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sabia-core"
|
||||
version = "0.1.0"
|
||||
version = "1.0.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"flate2",
|
||||
"hmac",
|
||||
"libc",
|
||||
"md-5",
|
||||
@@ -465,6 +532,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
|
||||
@@ -3,10 +3,10 @@ resolver = "2"
|
||||
members = ["crates/sabia-core", "crates/sabia-cli"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
version = "1.0.3"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/lucasrguerra/sabia"
|
||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
||||
authors = ["Sabiá contributors"]
|
||||
rust-version = "1.80"
|
||||
|
||||
@@ -15,3 +15,7 @@ lto = "thin"
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
@@ -1,21 +1,25 @@
|
||||
<p align="center"><img src="apps/gui/ui/sabia.png" width="300" alt="Sabiá"></p>
|
||||
<p align="center"><img src="logo.png" width="300" alt="Sabiá"></p>
|
||||
<h1 align="center">Sabiá</h1>
|
||||
<p align="center">Free and open Wi-Fi auditing.<br>Terminal and desktop app · live capture on Linux, offline analysis on Windows, Linux and macOS · authorized use.</p>
|
||||
<p align="center">Free and open Wi-Fi auditing.<br>Terminal and desktop app · live capture on Linux, offline analysis on Windows too · authorized use.</p>
|
||||
|
||||
<p align="center"><a href="docs/cli.md">Terminal</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/architecture.md">Architecture</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
|
||||
<p align="center"><a href="docs/cli.md">Terminal</a> · <a href="docs/interface.md">Interface</a> · <a href="docs/architecture.md">Architecture</a> · <a href="docs/support.md">Support</a> · <a href="ROADMAP.md">Roadmap</a> · <a href="CHANGELOG.md">Changelog</a></p>
|
||||
|
||||
<p align="center"><b>English</b> · <a href="README.pt-BR.md">Português</a></p>
|
||||
|
||||
<p align="center"><img src="docs/img/networks-light-en.png" width="900" alt="The Sabiá desktop app capturing Wi-Fi frames in monitor mode, showing visible networks with signal bars, channels, security badges and associated clients"><br><sub>The real interface running with synthetic audit data (simulated networks, no real third-party data).</sub></p>
|
||||
|
||||
---
|
||||
|
||||
Sabiá brings together, in a single sweep, what tools like `airodump-ng`, `hcxdumptool`, `aircrack-ng` and `wifite` do separately — in one Rust binary, with no `libpcap` dependency:
|
||||
|
||||
- **Network discovery:** hops across the **2.4, 5 and 6 GHz** channels and lists access points and associated clients, with SSID, BSSID, channel, signal and security type.
|
||||
- **Monitor mode without scripts:** turns the card's monitor mode on and off straight through netlink/`iw`, and shows which channels the radio supports.
|
||||
- **Handshake capture:** recognizes the **4-way handshake (EAPOL)** and the **PMKID** on the air and keeps only what offline cracking needs.
|
||||
- **Offline dictionary cracking:** recomputes the PMK with **PBKDF2-HMAC-SHA1** and checks the **MIC** (4-way) or the **PMKID** (`HMAC-SHA1(PMK, "PMK Name" …)`) — for WPA/WPA2-PSK.
|
||||
- **Network discovery:** hops across the **2.4, 5 and 6 GHz** channels and lists access points and associated clients, with SSID, BSSID, channel, signal, security type, WPS details and vendor.
|
||||
- **Monitor mode without scripts:** turns the card's monitor mode on and off straight through netlink/`iw`, recovering automatically if NetworkManager tries to reclaim the interface, and shows which channels the radio supports.
|
||||
- **Handshake capture:** recognizes the **4-way handshake (EAPOL)** and the **PMKID** on the air and keeps only what offline cracking needs; decrypts CCMP traffic for a client once its password is known.
|
||||
- **Cracking, three ways:** dictionary (9.7M passwords bundled by default, rockyou + Probable-Wordlists), hashcat-style **mutation rules**, and **mask brute force** (`?d?l?u?s?a`) — recomputes the PMK with **PBKDF2-HMAC-SHA1** and checks the **MIC** (4-way) or the **PMKID** — for WPA/WPA2-PSK. Export to **`.hc22000`** to hand the handshake to hashcat/John instead.
|
||||
- **pcap round-trip:** writes captured frames to `.pcap` and **analyzes a `.pcap` offline** (networks, clients and handshakes), interoperable with the other tools.
|
||||
- **Deauthentication test:** sends _deauth_ to an AP/station to force a reassociation — only with an explicit authorization confirmation (`--yes`).
|
||||
- **Deauthentication and WPS:** directional/continuous _deauth_, PMF probing, clientless PMKID hunting, and a WPS PIN/Pixie Dust attack (bundled reaver, or bully when installed) — all gated behind an explicit authorization confirmation (`--yes` / checkbox).
|
||||
- **Evil Twin:** clones an already-seen AP with a captive portal for WPA/WPA2-PSK — the submitted password is only accepted after it's verified against the real captured handshake, never "accepts anything" — plus an Enterprise (802.1X/EAP) variant that captures credentials automatically, no portal needed.
|
||||
- **Audit report:** a paginated, printable report (cover, table of contents, findings, channel scoring, audit trail) generated locally.
|
||||
- **Audit trail:** every monitor-mode activation and capture session is recorded in a local **hash-chained** trail.
|
||||
- **Diagnostics:** `doctor` checks privileges, tools and interfaces before you start.
|
||||
|
||||
@@ -35,17 +39,18 @@ Download the package for your system from **[Releases](../../releases)**:
|
||||
|
||||
| | CLI | Desktop app |
|
||||
|---|---|---|
|
||||
| Windows | `sabia-*-windows-x86_64.zip` | `.msi` / `.exe` installer |
|
||||
| macOS | `sabia-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` |
|
||||
| Windows | — | `.exe` installer |
|
||||
| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
||||
|
||||
Each file has a `.sha256` next to it. Build from source: `cargo build --release -p sabia-cli` (Rust 1.80+).
|
||||
The CLI is Linux-only; the desktop app ships for Linux and Windows. Each CLI
|
||||
package has a `.sha256` next to it. Build from source: `cargo build --release
|
||||
-p sabia-cli` (Rust 1.80+, Linux).
|
||||
|
||||
**Updates:** the desktop app checks GitHub on startup; when a new version exists, a button offers **Update now**: it downloads the right binary for your platform, verifies the **SHA-256** and swaps the running executable — no manual download. The check is a single request to the releases API and sends nothing of yours.
|
||||
**Updates:** the desktop app checks Gitea on startup; when a new version exists, a button offers **Update now**. The CLI updates the same way (`sabia update`); it downloads the right binary for your platform, verifies the **SHA-256** and swaps the running executable — no manual download. The check is a single request to the releases API and sends nothing of yours.
|
||||
|
||||
> The binaries are **not signed yet**: Windows SmartScreen and macOS Gatekeeper will warn. Check the SHA-256.
|
||||
> The binaries are **not signed yet**: Windows SmartScreen will warn. Check the SHA-256.
|
||||
|
||||
**Capture and monitor mode require Linux and root** (`CAP_NET_ADMIN`/`CAP_NET_RAW`): raw capture uses `AF_PACKET` and interface control uses netlink. **Offline `.pcap` analysis and cracking** also run on Windows and macOS. The desktop app elevates to administrator on its own at startup.
|
||||
**Capture and monitor mode require Linux and root** — the only platform the CLI ships for (`CAP_NET_ADMIN`/`CAP_NET_RAW`): raw capture uses `AF_PACKET` and interface control uses netlink. **Offline `.pcap` analysis and cracking** also run on Windows and macOS. The desktop app elevates to administrator on its own at startup.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -65,6 +70,8 @@ Authorized deauthentication testing and all other commands (`audit`, `update`, `
|
||||
|
||||
Desktop app (Tauri) with an interface sidebar, a capture panel with an animated signal line, and a networks table with signal bars and color-coded security badges. The same front-end runs in the browser with `sabia ui`. More in **[docs/interface.md](docs/interface.md)**.
|
||||
|
||||
<p align="center"><img src="docs/img/networks-dark-en.png" width="640" alt="The networks table in the dark theme"> <img src="docs/img/attack-dark-en.png" width="640" alt="Network detail dialog showing attacks and authorization"></p>
|
||||
|
||||
- Pick the interface, toggle **monitor mode** and start/stop **capture** by button.
|
||||
- Live **networks seen** table, with signal, channel and security.
|
||||
- An **authorized-use** notice always in view and automatic privilege elevation on startup.
|
||||
@@ -72,11 +79,11 @@ Desktop app (Tauri) with an interface sidebar, a capture panel with an animated
|
||||
|
||||
## Known limits
|
||||
|
||||
- **Capture is Linux-only**, with a card that supports **monitor mode** (and injection, for `deauth`). On Windows and macOS Sabiá only does `.pcap` analysis and offline cracking.
|
||||
- **Capture is Linux-only**, with a card that supports **monitor mode** (and injection, for `deauth`); the CLI is not built for other platforms. On Windows Sabiá's desktop app only does `.pcap` analysis and offline cracking.
|
||||
- **WPA3/SAE** does not fall to handshake dictionaries — cracking covers **WPA/WPA2-PSK** (4-way and PMKID).
|
||||
- **5 and 6 GHz** depend on the card and the regulatory domain; some channels stay passive (no probing).
|
||||
- **Passwords outside the dictionary don't come back:** cracking is dictionary force only, no rules or masks yet.
|
||||
- **Windows and macOS:** the code compiles and is tested in CI, but live capture does not run on those platforms.
|
||||
- **Evil Twin's self-signed certificate:** the Enterprise variant's TLS certificate isn't trusted by clients that validate the chain (common on updated corporate/MDM devices) — that's the correct defense working, not a bug.
|
||||
- **Windows:** the code compiles and is tested in CI, but live capture does not run there; the desktop app does offline analysis only. There is no macOS build: Tauri needs the macOS SDK and code signing, both tied to Apple hardware and an Apple Developer account, which this project's CI (a single Linux runner) does not have.
|
||||
|
||||
## Where it's going
|
||||
|
||||
@@ -84,10 +91,10 @@ Summary of the [ROADMAP](ROADMAP.md):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Done** | Monitor mode via netlink, 2.4/5/6 GHz scanning, 4-way and PMKID capture, `.pcap` read/write, dictionary cracking (WPA/WPA2-PSK), authorized `deauth`, hash-chained audit trail, `doctor`, CLI + desktop app, one-click update |
|
||||
| **Next** | Rules and masks in cracking, GPU acceleration, export to the hashcat format (`22000`) |
|
||||
| **Later** | WPS attack, rogue AP / evil twin for authorized testing, capture on Windows (Npcap) |
|
||||
| **Long term** | WPA3/SAE downgrade, validation on varied hardware |
|
||||
| **Done** | Monitor mode via netlink (with NetworkManager recovery), 2.4/5/6 GHz scanning, 4-way and PMKID capture, CCMP decryption, dictionary/rules/mask cracking (WPA/WPA2-PSK), `.hc22000` export, `.pcap` read/write, authorized `deauth`/PMF/PMKID, WPS attack, Evil Twin (PSK + Enterprise), channel scoring and findings, paginated audit report, hash-chained audit trail, `doctor`, CLI + desktop app, one-click update |
|
||||
| **Next** | GPU acceleration for cracking, capture on Windows (Npcap) |
|
||||
| **Not planned** | A macOS build — see [Known limits](#known-limits) |
|
||||
| **Later** | WPA3/SAE downgrade, validation on varied hardware, signed release binaries |
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
<p align="center"><img src="apps/gui/ui/sabia.png" width="300" alt="Sabiá"></p>
|
||||
<p align="center"><img src="logo.png" width="300" alt="Sabiá"></p>
|
||||
<h1 align="center">Sabiá</h1>
|
||||
<p align="center">Auditoria de Wi-Fi livre e gratuita.<br>Terminal e interface gráfica · captura no Linux, análise offline em Windows, Linux e macOS · uso autorizado.</p>
|
||||
<p align="center">Auditoria de Wi-Fi livre e gratuita.<br>Terminal e interface gráfica · captura no Linux, análise offline também em Windows · uso autorizado.</p>
|
||||
|
||||
<p align="center"><a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
|
||||
<p align="center"><a href="docs/pt-BR/cli.md">Terminal</a> · <a href="docs/pt-BR/interface.md">Interface</a> · <a href="docs/pt-BR/arquitetura.md">Arquitetura</a> · <a href="docs/pt-BR/suporte.md">Suporte</a> · <a href="ROADMAP.pt-BR.md">Roadmap</a> · <a href="CHANGELOG.pt-BR.md">Changelog</a></p>
|
||||
|
||||
<p align="center"><a href="README.md">English</a> · <b>Português</b></p>
|
||||
|
||||
<p align="center"><img src="docs/img/networks-light-pt.png" width="900" alt="O aplicativo desktop do Sabiá capturando quadros Wi-Fi em modo monitor, mostrando redes visíveis com barras de sinal, canais, selos de segurança e clientes associados"><br><sub>A interface de verdade rodando com dados sintéticos de auditoria (redes simuladas, sem dados reais de terceiros).</sub></p>
|
||||
|
||||
---
|
||||
|
||||
O Sabiá junta, numa única varredura, o que ferramentas como `airodump-ng`, `hcxdumptool`, `aircrack-ng` e `wifite` fazem separadamente — em um só binário Rust, sem depender de `libpcap`:
|
||||
|
||||
- **Descoberta de redes:** percorre os canais de **2.4, 5 e 6 GHz** e lista os pontos de acesso e os clientes associados, com SSID, BSSID, canal, sinal e tipo de segurança.
|
||||
- **Modo monitor sem scripts:** liga e desliga o modo monitor da placa direto pelo netlink/`iw`, e mostra os canais que o rádio suporta.
|
||||
- **Captura de handshake:** reconhece o **4-way handshake (EAPOL)** e o **PMKID** no ar e guarda só o que é preciso para a quebra offline.
|
||||
- **Quebra offline por dicionário:** recalcula a PMK com **PBKDF2-HMAC-SHA1** e confere o **MIC** (4-way) ou o **PMKID** (`HMAC-SHA1(PMK, "PMK Name" …)`) — para WPA/WPA2-PSK.
|
||||
- **Descoberta de redes:** percorre os canais de **2.4, 5 e 6 GHz** e lista os pontos de acesso e os clientes associados, com SSID, BSSID, canal, sinal, tipo de segurança, detalhes de WPS e fabricante.
|
||||
- **Modo monitor sem scripts:** liga e desliga o modo monitor da placa direto pelo netlink/`iw`, com recuperação automática se o NetworkManager tentar retomar a interface, e mostra os canais que o rádio suporta.
|
||||
- **Captura de handshake:** reconhece o **4-way handshake (EAPOL)** e o **PMKID** no ar e guarda só o que é preciso para a quebra offline; decifra tráfego CCMP de um cliente assim que a senha dele é conhecida.
|
||||
- **Quebra, de três jeitos:** dicionário (9,7 milhões de senhas por padrão, rockyou + Probable-Wordlists), **regras de mutação** estilo hashcat e **força bruta por máscara** (`?d?l?u?s?a`) — recalcula a PMK com **PBKDF2-HMAC-SHA1** e confere o **MIC** (4-way) ou o **PMKID** — para WPA/WPA2-PSK. Exporta em **`.hc22000`** para levar o handshake direto ao hashcat/John.
|
||||
- **pcap de ida e volta:** grava os quadros capturados em `.pcap` e **analisa um `.pcap` offline** (redes, clientes e handshakes), compatível com as outras ferramentas.
|
||||
- **Teste de desautenticação:** envia _deauth_ a um AP/estação para forçar uma reassociação — só com a confirmação explícita de autorização (`--yes`).
|
||||
- **Desautenticação e WPS:** _deauth_ direcional/contínuo, teste de PMF, caça de PMKID sem cliente, e ataque WPS PIN/Pixie Dust (reaver embutido, ou bully quando instalado) — tudo atrás de confirmação explícita de autorização (`--yes`/checkbox).
|
||||
- **Evil Twin:** clona um AP já visto com portal cativo para WPA/WPA2-PSK — a senha só é aceita depois de conferida contra o handshake real já capturado, nunca "aceita qualquer coisa" — mais uma variante Enterprise (802.1X/EAP) que captura credenciais automaticamente, sem portal.
|
||||
- **Relatório de auditoria:** relatório paginado e imprimível (capa, sumário, achados, pontuação de canais, trilha de auditoria) gerado localmente.
|
||||
- **Trilha de auditoria:** toda ativação de modo monitor e sessão de captura fica registrada numa trilha local **encadeada por hash**.
|
||||
- **Diagnóstico:** o `doctor` confere privilégios, ferramentas e interfaces antes de você começar.
|
||||
|
||||
@@ -35,17 +39,18 @@ Baixe o pacote do seu sistema em **[Releases](../../releases)**:
|
||||
|
||||
| | CLI | App gráfico |
|
||||
|---|---|---|
|
||||
| Windows | `sabia-*-windows-x86_64.zip` | instalador `.msi` / `.exe` |
|
||||
| macOS | `sabia-*-macos-{aarch64,x86_64}.tar.gz` | `.dmg` |
|
||||
| Windows | — | instalador `.exe` |
|
||||
| Linux | `sabia-*-linux-{x86_64,aarch64}.tar.gz` | `.AppImage` / `.deb` |
|
||||
|
||||
Cada arquivo tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p sabia-cli` (Rust 1.80+).
|
||||
O CLI é só para Linux; o app desktop sai para Linux e Windows. Cada pacote do
|
||||
CLI tem um `.sha256` ao lado. Compilar do código: `cargo build --release -p
|
||||
sabia-cli` (Rust 1.80+, Linux).
|
||||
|
||||
**Atualizações:** o app desktop consulta o GitHub ao abrir; havendo versão nova, um botão oferece **Atualizar agora**: ele baixa o binário certo para a plataforma, confere o **SHA-256** e troca o executável em uso — você não precisa baixar nada na mão. A consulta é uma única requisição à API de releases e nada seu é enviado.
|
||||
**Atualizações:** o app desktop consulta o Gitea ao abrir; havendo versão nova, um botão oferece **Atualizar agora**. O CLI atualiza do mesmo jeito (`sabia update`); ele baixa o binário certo para a plataforma, confere o **SHA-256** e troca o executável em uso — você não precisa baixar nada na mão. A consulta é uma única requisição à API de releases e nada seu é enviado.
|
||||
|
||||
> Os binários ainda **não são assinados**: o SmartScreen (Windows) e o Gatekeeper (macOS) vão avisar. Confira o SHA-256.
|
||||
> Os binários ainda **não são assinados**: o SmartScreen do Windows vai avisar. Confira o SHA-256.
|
||||
|
||||
**Captura e modo monitor exigem Linux e privilégio de root** (`CAP_NET_ADMIN`/`CAP_NET_RAW`): a captura crua usa `AF_PACKET` e o controle de interface usa netlink. A **análise de `.pcap` e a quebra offline** rodam também em Windows e macOS. O app desktop pede elevação de administrador sozinho ao abrir.
|
||||
**Captura e modo monitor exigem Linux e privilégio de root** — a única plataforma para a qual o CLI é compilado (`CAP_NET_ADMIN`/`CAP_NET_RAW`): a captura crua usa `AF_PACKET` e o controle de interface usa netlink. A **análise de `.pcap` e a quebra offline** rodam também em Windows e macOS. O app desktop pede elevação de administrador sozinho ao abrir.
|
||||
|
||||
## Começando
|
||||
|
||||
@@ -65,6 +70,8 @@ Teste autorizado de desautenticação e todos os outros comandos (`audit`, `upda
|
||||
|
||||
App desktop (Tauri) com sidebar de interfaces, painel de captura com linha de sinal animada e uma tabela de redes com barras de sinal e selos de segurança por cor. O mesmo front-end roda no navegador com `sabia ui`. Mais detalhes em **[docs/pt-BR/interface.md](docs/pt-BR/interface.md)**.
|
||||
|
||||
<p align="center"><img src="docs/img/networks-dark-pt.png" width="640" alt="A tabela de redes no tema escuro"> <img src="docs/img/attack-dark-pt.png" width="640" alt="Diálogo de detalhes da rede com opções de ataque e autorização"></p>
|
||||
|
||||
- Seleção da interface, ativação de **modo monitor** e início/parada de **captura** por botão.
|
||||
- Tabela de **redes vistas** em tempo real, com sinal, canal e segurança.
|
||||
- Aviso de **uso autorizado** sempre à vista e elevação automática de privilégio ao abrir.
|
||||
@@ -72,11 +79,11 @@ App desktop (Tauri) com sidebar de interfaces, painel de captura com linha de si
|
||||
|
||||
## Limites conhecidos
|
||||
|
||||
- **Captura só no Linux**, com placa que suporte **modo monitor** (e injeção, para `deauth`). Em Windows e macOS o Sabiá faz só análise de `.pcap` e quebra offline.
|
||||
- **Captura só no Linux**, com placa que suporte **modo monitor** (e injeção, para `deauth`); o CLI não é compilado para outras plataformas. Em Windows o app desktop do Sabiá faz só análise de `.pcap` e quebra offline.
|
||||
- **WPA3/SAE** não cai por dicionário de handshake — a quebra cobre **WPA/WPA2-PSK** (4-way e PMKID).
|
||||
- **5 e 6 GHz** dependem da placa e do _regulatory domain_; alguns canais ficam passivos (sem _probe_).
|
||||
- **Senhas fora do dicionário não voltam:** a quebra é só força de dicionário, sem regras nem máscara ainda.
|
||||
- **Windows e macOS:** o código compila e é testado em CI, mas a captura ao vivo não roda nessas plataformas.
|
||||
- **Certificado autoassinado do Evil Twin Enterprise:** clientes que validam a cadeia de certificado da rede (comum em perfil corporativo/MDM atualizado) não confiam nele — é a defesa correta funcionando, não um bug.
|
||||
- **Windows:** o código compila e é testado em CI, mas a captura ao vivo não roda lá; o app desktop faz só análise offline. Não há build para macOS: o Tauri precisa do SDK do macOS e de assinatura, os dois presos a hardware Apple e a uma conta Apple Developer, que o CI deste projeto (um único runner Linux) não tem.
|
||||
|
||||
## Para onde vai
|
||||
|
||||
@@ -84,10 +91,10 @@ Resumo do [ROADMAP](ROADMAP.pt-BR.md):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Feito** | Modo monitor por netlink, varredura 2.4/5/6 GHz, captura de 4-way e PMKID, escrita e leitura de `.pcap`, quebra por dicionário (WPA/WPA2-PSK), `deauth` autorizado, trilha de auditoria por hash, `doctor`, CLI + app desktop, atualização em um clique |
|
||||
| **Próximo** | Regras e máscaras na quebra, aceleração por GPU, exportação para o formato do hashcat (`22000`) |
|
||||
| **Depois** | Ataque a WPS, _rogue AP_/_evil twin_ para teste autorizado, captura em Windows (Npcap) |
|
||||
| **Longo prazo** | WPA3/SAE _downgrade_, validação em hardware variado |
|
||||
| **Feito** | Modo monitor por netlink (com recuperação do NetworkManager), varredura 2.4/5/6 GHz, captura de 4-way e PMKID, decriptação CCMP, quebra por dicionário/regras/máscara (WPA/WPA2-PSK), exportação `.hc22000`, escrita e leitura de `.pcap`, `deauth`/PMF/PMKID autorizados, ataque WPS, Evil Twin (PSK + Enterprise), pontuação de canais e achados, relatório de auditoria paginado, trilha de auditoria por hash, `doctor`, CLI + app desktop, atualização em um clique |
|
||||
| **Próximo** | Aceleração por GPU na quebra, captura em Windows (Npcap) |
|
||||
| **Fora de plano** | Um build para macOS — veja [Limites conhecidos](#limites-conhecidos) |
|
||||
| **Depois** | WPA3/SAE _downgrade_, validação em hardware variado, binários de release assinados |
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
|
||||
@@ -2,34 +2,37 @@
|
||||
|
||||
**English** · [Português](ROADMAP.pt-BR.md)
|
||||
|
||||
Sabiá does Wi-Fi auditing: it discovers networks, captures handshakes (4-way and PMKID), cracks WPA/WPA2-PSK by offline dictionary and tests deauthentication with authorization. Always for authorized use, with a local audit trail.
|
||||
Sabiá does Wi-Fi auditing: it discovers networks, captures handshakes (4-way and PMKID), cracks WPA/WPA2-PSK by dictionary/rules/mask, assesses WPS, PMF and evil twin exposure, and tests deauthentication and rogue-AP attacks with authorization. Always for authorized use, with a local audit trail.
|
||||
|
||||
## Done (0.1.0)
|
||||
## Done (1.0.0)
|
||||
|
||||
- **Monitor mode** and channel control via netlink/`iw`.
|
||||
- **Scanning** of 2.4, 5 and 6 GHz: APs and clients, with SSID, BSSID, channel, signal and security.
|
||||
- **Monitor mode** and channel control via netlink/`iw`, with automatic recovery from NetworkManager reclaiming the interface.
|
||||
- **Scanning** of 2.4, 5 and 6 GHz: APs and clients, with SSID, BSSID, channel, signal, security, WPS details, vendor and PHY standard.
|
||||
- Raw 802.11 capture via **AF_PACKET** (no libpcap), **radiotap** and management-frame parsing.
|
||||
- Capture of the **4-way handshake (EAPOL)** and the **PMKID**.
|
||||
- **Offline dictionary cracking** (PBKDF2-HMAC-SHA1, MIC and PMKID) for WPA/WPA2-PSK.
|
||||
- Capture of the **4-way handshake (EAPOL)** and the **PMKID**; **CCMP traffic decryption** once the password is known.
|
||||
- **Offline cracking** (PBKDF2-HMAC-SHA1, MIC and PMKID) for WPA/WPA2-PSK: a 9.7M-password dictionary (rockyou + Probable-Wordlists) by default, hashcat-style **mutation rules** and **mask brute force**.
|
||||
- **`.hc22000` export** to take the handshake straight to hashcat/John.
|
||||
- **`.pcap`** read and write; offline `.pcap` analysis.
|
||||
- Authorized **`deauth`** (`--yes` flag).
|
||||
- Authorized **`deauth`** (`--yes` flag), directional and continuous jobs, PMF probing, clientless PMKID hunting.
|
||||
- **WPS attack** (PIN/Pixie Dust) via a bundled reaver, or bully when installed.
|
||||
- **Evil Twin** with captive portal for WPA/WPA2-PSK (password only accepted after cryptographic verification against the real handshake) and a credential-capture variant for WPA2/WPA3-Enterprise (802.1X/EAP).
|
||||
- Channel-interference scoring and security findings (open network, no PMF, WPS exposed, possible evil twin).
|
||||
- Redesigned, paginated **audit report** (cover, table of contents, running header/footer) that prints to PDF from the browser.
|
||||
- Local hash-chained **audit trail**; `doctor` diagnostics.
|
||||
- **CLI** + **desktop app** (Tauri) + local UI server; **one-click update**.
|
||||
|
||||
## Next
|
||||
|
||||
- **Rules and masks** in cracking (hashcat style), beyond the plain dictionary.
|
||||
- **`22000` export** to take the handshake straight to hashcat/John.
|
||||
- **GPU** acceleration for PMK derivation.
|
||||
|
||||
## Later
|
||||
|
||||
- **WPS attack** (PIN/Pixie Dust) in authorized testing.
|
||||
- **Rogue AP / evil twin** for client assessment, with authorization.
|
||||
- **Capture on Windows** via Npcap.
|
||||
|
||||
## Long term
|
||||
## Later
|
||||
|
||||
- **WPA3/SAE**: detection and downgrade paths where applicable.
|
||||
- Validation on **varied hardware** (chipsets and regulatory domains).
|
||||
- Signing of release binaries (SmartScreen/Gatekeeper).
|
||||
|
||||
## Long term
|
||||
|
||||
- GPU-accelerated cracking backend (hashcat integration beyond export).
|
||||
- Packaged distribution (`apt`/`brew`/winget).
|
||||
@@ -2,34 +2,37 @@
|
||||
|
||||
[English](ROADMAP.md) · **Português**
|
||||
|
||||
O Sabiá faz auditoria de Wi-Fi: descobre redes, captura handshakes (4-way e PMKID), quebra WPA/WPA2-PSK por dicionário offline e testa desautenticação com autorização. Sempre de uso autorizado, com trilha de auditoria local.
|
||||
O Sabiá faz auditoria de Wi-Fi: descobre redes, captura handshakes (4-way e PMKID), quebra WPA/WPA2-PSK por dicionário/regras/máscara, avalia exposição a WPS, PMF e evil twin, e testa desautenticação e ataques de rogue AP com autorização. Sempre de uso autorizado, com trilha de auditoria local.
|
||||
|
||||
## Feito (0.1.0)
|
||||
## Feito (1.0.0)
|
||||
|
||||
- Controle de **modo monitor** e de canal por netlink/`iw`.
|
||||
- **Varredura** de 2.4, 5 e 6 GHz: APs e clientes, com SSID, BSSID, canal, sinal e segurança.
|
||||
- Controle de **modo monitor** e de canal por netlink/`iw`, com recuperação automática quando o NetworkManager tenta retomar a interface.
|
||||
- **Varredura** de 2.4, 5 e 6 GHz: APs e clientes, com SSID, BSSID, canal, sinal, segurança, detalhes de WPS, fabricante e padrão PHY.
|
||||
- Captura crua de 802.11 por **AF_PACKET** (sem libpcap), parsing de **radiotap** e de quadros de gerenciamento.
|
||||
- Captura de **4-way handshake (EAPOL)** e de **PMKID**.
|
||||
- **Quebra offline** por dicionário (PBKDF2-HMAC-SHA1, MIC e PMKID) para WPA/WPA2-PSK.
|
||||
- Captura de **4-way handshake (EAPOL)** e de **PMKID**; **decriptação de tráfego CCMP** quando a senha é conhecida.
|
||||
- **Quebra offline** (PBKDF2-HMAC-SHA1, MIC e PMKID) para WPA/WPA2-PSK: dicionário padrão de 9,7 milhões de senhas (rockyou + Probable-Wordlists), **regras de mutação** estilo hashcat e **força bruta por máscara**.
|
||||
- **Exportação `.hc22000`** para levar o handshake direto ao hashcat/John.
|
||||
- Escrita e leitura de **`.pcap`**; análise de `.pcap` offline.
|
||||
- **`deauth`** autorizado (flag `--yes`).
|
||||
- **`deauth`** autorizado (flag `--yes`), jobs direcionais e contínuos, teste de PMF, caça de PMKID sem cliente.
|
||||
- **Ataque WPS** (PIN/Pixie Dust) via reaver embutido, ou bully quando instalado.
|
||||
- **Evil Twin** com portal cativo para WPA/WPA2-PSK (senha só aceita após verificação criptográfica contra o handshake real) e variante de captura automática de credencial para WPA2/WPA3-Enterprise (802.1X/EAP).
|
||||
- Pontuação de interferência de canais e achados de segurança (rede aberta, sem PMF, WPS exposto, possível evil twin).
|
||||
- **Relatório de auditoria** redesenhado e paginado (capa, sumário, cabeçalho/rodapé corridos) que imprime em PDF pelo navegador.
|
||||
- **Trilha de auditoria** local encadeada por hash; `doctor` de diagnóstico.
|
||||
- **CLI** + **app desktop** (Tauri) + servidor local da UI; **atualização em um clique**.
|
||||
|
||||
## Próximo
|
||||
|
||||
- **Regras e máscaras** na quebra (estilo hashcat), além do dicionário puro.
|
||||
- **Exportação `22000`** para levar o handshake direto ao hashcat/John.
|
||||
- Aceleração por **GPU** na derivação da PMK.
|
||||
|
||||
## Depois
|
||||
|
||||
- **Ataque a WPS** (PIN/Pixie Dust) em teste autorizado.
|
||||
- **Rogue AP / evil twin** para avaliação de clientes, com autorização.
|
||||
- **Captura em Windows** via Npcap.
|
||||
|
||||
## Longo prazo
|
||||
## Depois
|
||||
|
||||
- **WPA3/SAE**: detecção e caminhos de _downgrade_ onde aplicável.
|
||||
- Validação em **hardware variado** (chipsets e _regulatory domains_).
|
||||
- Assinatura dos binários de release (SmartScreen/Gatekeeper).
|
||||
|
||||
## Longo prazo
|
||||
|
||||
- Backend de quebra acelerado por GPU (integração com hashcat além da exportação).
|
||||
- Distribuição empacotada (`apt`/`brew`/winget).
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
**English** · [Português](SECURITY.pt-BR.md)
|
||||
|
||||
Sabiá processes untrusted data from the air and from `.pcap` files (802.11 frames and handshakes, possibly forged) and controls network interfaces with root privilege. Vulnerabilities — memory corruption in the radiotap/802.11/EAPOL parsers, path escape when writing `.pcap` or exporting, privilege misuse — should be reported privately through the repository's **Security → Report a vulnerability** tab on GitHub. Do not open a public issue before a fix.
|
||||
Sabiá processes untrusted data from the air and from `.pcap` files (802.11 frames and handshakes, possibly forged) and controls network interfaces with root privilege. Vulnerabilities — memory corruption in the radiotap/802.11/EAPOL parsers, path escape when writing `.pcap` or exporting, privilege misuse — should be reported privately through the private vulnerability reporting feature of the repository on Gitea. Do not open a public issue before a fix.
|
||||
|
||||
Sabiá is a tool for **authorized auditing**. Reports about using it to attack third-party networks are out of the project's security scope — see the **Authorized use** section of the [README](README.md).
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
[English](SECURITY.md) · **Português**
|
||||
|
||||
O Sabiá processa dados não confiáveis vindos do ar e de arquivos `.pcap` (quadros 802.11 e handshakes, possivelmente forjados) e controla interfaces de rede com privilégio de root. Vulnerabilidades — corrupção de memória nos parsers de radiotap/802.11/EAPOL, escape de caminho na escrita de `.pcap` ou na exportação, uso indevido de privilégio — devem ser reportadas em privado pela aba **Security → Report a vulnerability** do repositório no GitHub. Não abra issue pública antes da correção.
|
||||
O Sabiá processa dados não confiáveis vindos do ar e de arquivos `.pcap` (quadros 802.11 e handshakes, possivelmente forjados) e controla interfaces de rede com privilégio de root. Vulnerabilidades — corrupção de memória nos parsers de radiotap/802.11/EAPOL, escape de caminho na escrita de `.pcap` ou na exportação, uso indevido de privilégio — devem ser reportadas em privado pelo recurso de relato privado de vulnerabilidade do repositório no Gitea. Não abra issue pública antes da correção.
|
||||
|
||||
O Sabiá é uma ferramenta de **auditoria autorizada**. Relatos sobre uso para atacar redes de terceiros não são escopo de segurança do projeto — veja a seção **Uso autorizado** do [README](README.pt-BR.md).
|
||||
@@ -8,6 +8,17 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
@@ -322,6 +333,16 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.9"
|
||||
@@ -1474,6 +1495,15 @@ dependencies = [
|
||||
"cfb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.2"
|
||||
@@ -2508,8 +2538,10 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "sabia-core"
|
||||
version = "0.1.0"
|
||||
version = "1.0.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"flate2",
|
||||
"hmac",
|
||||
"libc",
|
||||
"md-5",
|
||||
@@ -2523,7 +2555,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sabia-gui"
|
||||
version = "0.1.0"
|
||||
version = "1.0.3"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sabia-core",
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# App desktop (Tauri 2). Fica fora do workspace principal porque exige
|
||||
# as bibliotecas de WebView do sistema (WebKitGTK no Linux).
|
||||
# Desktop application (Tauri 2). Kept outside the main workspace because it
|
||||
# requires system WebView libraries (WebKitGTK on Linux).
|
||||
[package]
|
||||
name = "sabia-gui"
|
||||
description = "Sabiá: interface gráfica de auditoria de Wi-Fi"
|
||||
version = "0.1.0"
|
||||
description = "Sabiá: graphical Wi-Fi security auditing interface"
|
||||
version = "1.0.3"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/lucasrguerra/sabia"
|
||||
authors = ["Sabiá contributors"]
|
||||
repository = "https://git.lucasrguerra.dev.br/lucasrguerra/sabia"
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=../../../logo.png");
|
||||
let _ = std::fs::copy("../../../logo.png", "../ui/logo.png");
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// Modo monitor e captura crua exigem administrador no Windows: o
|
||||
// manifesto pede elevação (UAC) assim que o app abre.
|
||||
// Monitor mode and raw frame capture require Administrator privileges on Windows:
|
||||
// embed UAC elevation manifest at startup.
|
||||
let manifest = include_str!("windows-app-manifest.xml");
|
||||
let attrs = tauri_build::Attributes::new()
|
||||
.windows_attributes(tauri_build::WindowsAttributes::new().app_manifest(manifest));
|
||||
tauri_build::try_build(attrs).expect("falha no tauri-build");
|
||||
tauri_build::try_build(attrs).expect("tauri-build failed");
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
tauri_build::build();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Main window permissions",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default", "core:window:allow-destroy", "core:window:allow-close"]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{}
|
||||
{"default":{"identifier":"default","description":"Main window permissions","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-destroy","core:window:allow-close"]}}
|
||||
|
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,7 +1,7 @@
|
||||
// Evita a janela de console extra no Windows em release.
|
||||
// Prevents an extra console window on Windows in release builds.
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use sabia_core::engine::Engine;
|
||||
use sabia_core::engine::{Engine, ScanOptions};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct State(Arc<Engine>);
|
||||
@@ -19,7 +19,10 @@ fn list_ifaces(state: tauri::State<'_, State>) -> Result<serde_json::Value, Stri
|
||||
|
||||
#[tauri::command]
|
||||
fn set_monitor(state: tauri::State<'_, State>, iface: String, enable: bool) -> Result<(), String> {
|
||||
state.0.set_monitor(&iface, enable).map_err(|e| e.to_string())
|
||||
state
|
||||
.0
|
||||
.set_monitor(&iface, enable)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -33,10 +36,465 @@ fn stop_scan(state: tauri::State<'_, State>, iface: String) {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_results(state: tauri::State<'_, State>, iface: String) -> Result<serde_json::Value, String> {
|
||||
fn scan_results(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.scan_results(&iface)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_snapshot(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let snapshot = state.0.scan_snapshot(&iface).map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(snapshot).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_clients(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.scan_clients(&iface)).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn handshakes(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: Option<String>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.handshakes(iface.as_deref())).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn export_handshake(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
station: String,
|
||||
out: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.export_handshake(&iface, &bssid, &station, &out)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn export_all_handshakes(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
out: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.export_all_handshakes(&iface, &out)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_crack(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
wordlist: Option<String>,
|
||||
bssid: Option<String>,
|
||||
station: Option<String>,
|
||||
rules: Option<bool>,
|
||||
) -> Result<String, String> {
|
||||
let rules = rules.unwrap_or(false);
|
||||
let result = match wordlist.filter(|w| !w.trim().is_empty()) {
|
||||
Some(wordlist) => state.0.start_crack(
|
||||
&iface,
|
||||
bssid.as_deref(),
|
||||
station.as_deref(),
|
||||
&wordlist,
|
||||
rules,
|
||||
),
|
||||
None => state
|
||||
.0
|
||||
.start_crack_default(&iface, bssid.as_deref(), station.as_deref(), rules),
|
||||
};
|
||||
result.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_crack_mask(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
mask: String,
|
||||
bssid: Option<String>,
|
||||
station: Option<String>,
|
||||
max_candidates: Option<u64>,
|
||||
) -> Result<String, String> {
|
||||
state
|
||||
.0
|
||||
.start_crack_mask(
|
||||
&iface,
|
||||
bssid.as_deref(),
|
||||
station.as_deref(),
|
||||
&mask,
|
||||
max_candidates.unwrap_or(50_000_000),
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn crack_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
|
||||
match state.0.crack_job(&id) {
|
||||
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
|
||||
None => Err("job não encontrado".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn crack_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.crack_jobs()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn deauth(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
station: Option<String>,
|
||||
count: Option<u32>,
|
||||
reason: Option<u16>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.deauth(
|
||||
&iface,
|
||||
&bssid,
|
||||
station.as_deref(),
|
||||
count.unwrap_or(5),
|
||||
reason.unwrap_or(7),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn deauth_directional(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
station: Option<String>,
|
||||
count: Option<u32>,
|
||||
reason: Option<u16>,
|
||||
direction: Option<String>,
|
||||
frame_kind: Option<String>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.deauth_directional(
|
||||
&iface,
|
||||
&bssid,
|
||||
station.as_deref(),
|
||||
count.unwrap_or(5),
|
||||
reason.unwrap_or(7),
|
||||
direction.as_deref().unwrap_or("ap"),
|
||||
frame_kind.as_deref().unwrap_or("deauth"),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn start_deauth_job(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
station: Option<String>,
|
||||
reason: Option<u16>,
|
||||
direction: Option<String>,
|
||||
frame_kind: Option<String>,
|
||||
interval_ms: Option<u64>,
|
||||
max_secs: Option<u64>,
|
||||
stop_on_handshake: Option<bool>,
|
||||
) -> Result<String, String> {
|
||||
state
|
||||
.0
|
||||
.start_deauth_job(
|
||||
&iface,
|
||||
&bssid,
|
||||
station.as_deref(),
|
||||
reason.unwrap_or(7),
|
||||
direction.as_deref().unwrap_or("both"),
|
||||
frame_kind.as_deref().unwrap_or("deauth"),
|
||||
interval_ms.unwrap_or(120),
|
||||
max_secs,
|
||||
stop_on_handshake.unwrap_or(false),
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_deauth_job(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
|
||||
state.0.stop_deauth_job(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn deauth_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
|
||||
match state.0.deauth_job(&id) {
|
||||
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
|
||||
None => Err("job de deauth não encontrado".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn deauth_jobs(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: Option<String>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let jobs = state.0.deauth_jobs();
|
||||
let jobs: Vec<_> = match iface {
|
||||
Some(iface) => jobs
|
||||
.into_iter()
|
||||
.filter(|j| j.iface.eq_ignore_ascii_case(&iface))
|
||||
.collect(),
|
||||
None => jobs,
|
||||
};
|
||||
serde_json::to_value(jobs).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn capture_handshake(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
station: Option<String>,
|
||||
seconds: Option<u64>,
|
||||
) -> Result<String, String> {
|
||||
state
|
||||
.0
|
||||
.capture_handshake(&iface, &bssid, station.as_deref(), seconds)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn report(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let model = state.0.report(&iface).map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(model).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn report_html(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
inline: Option<bool>,
|
||||
) -> Result<String, String> {
|
||||
let model = state.0.report(&iface).map_err(|e| e.to_string())?;
|
||||
let options = sabia_core::report::ReportOptions {
|
||||
inline_pagedjs: inline.unwrap_or(false),
|
||||
..Default::default()
|
||||
};
|
||||
Ok(sabia_core::report::render(&model, options))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn export_hc22000(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
station: String,
|
||||
out: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.export_hc22000(&iface, &bssid, &station, &out)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn export_all_hc22000(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
out: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.export_all_hc22000(&iface, &out)
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn pmkid_hunt(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
attempts: Option<u32>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.pmkid_hunt(&iface, &bssid, attempts.unwrap_or(5))
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn pmf_test(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
station: String,
|
||||
reason: Option<u16>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let report = state
|
||||
.0
|
||||
.pmf_test(&iface, &bssid, &station, reason.unwrap_or(7))
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_value(report).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn wps_tools(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.wps_tools()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_wps(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
bssid: String,
|
||||
channel: u32,
|
||||
tool: String,
|
||||
pin: Option<String>,
|
||||
) -> Result<String, String> {
|
||||
state
|
||||
.0
|
||||
.start_wps(&iface, &bssid, channel, &tool, pin.as_deref())
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn wps_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
|
||||
match state.0.wps_job(&id) {
|
||||
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
|
||||
None => Err("job WPS não encontrado".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn wps_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.wps_jobs()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_wps(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
|
||||
state.0.stop_wps(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_evil_twin(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
ap_iface: String,
|
||||
bssid: String,
|
||||
portal_port: Option<u16>,
|
||||
deauth: Option<bool>,
|
||||
) -> Result<String, String> {
|
||||
state
|
||||
.0
|
||||
.start_evil_twin(
|
||||
&iface,
|
||||
&ap_iface,
|
||||
&bssid,
|
||||
portal_port.unwrap_or(8080),
|
||||
deauth.unwrap_or(false),
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn eviltwin_job(state: tauri::State<'_, State>, id: String) -> Result<serde_json::Value, String> {
|
||||
match state.0.eviltwin_job(&id) {
|
||||
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
|
||||
None => Err("job de Evil Twin não encontrado".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn eviltwin_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.eviltwin_jobs()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_evil_twin(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
|
||||
state.0.stop_evil_twin(&id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_evil_twin_enterprise(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
ap_iface: String,
|
||||
bssid: String,
|
||||
) -> Result<String, String> {
|
||||
state
|
||||
.0
|
||||
.start_evil_twin_enterprise(&iface, &ap_iface, &bssid)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn eviltwin_enterprise_job(
|
||||
state: tauri::State<'_, State>,
|
||||
id: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
match state.0.enterprise_twin_job(&id) {
|
||||
Some(job) => serde_json::to_value(job).map_err(|e| e.to_string()),
|
||||
None => Err("job de Evil Twin Enterprise não encontrado".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn eviltwin_enterprise_jobs(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(state.0.enterprise_twin_jobs()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn stop_evil_twin_enterprise(state: tauri::State<'_, State>, id: String) -> Result<(), String> {
|
||||
state
|
||||
.0
|
||||
.stop_evil_twin_enterprise(&id)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn start_scan_opts(
|
||||
state: tauri::State<'_, State>,
|
||||
iface: String,
|
||||
channel: Option<u32>,
|
||||
band: Option<String>,
|
||||
hop_ms: Option<u64>,
|
||||
pcap_out: Option<String>,
|
||||
seconds: Option<u64>,
|
||||
) -> Result<(), String> {
|
||||
let options = ScanOptions {
|
||||
channel,
|
||||
band,
|
||||
hop_ms: hop_ms.unwrap_or(350),
|
||||
pcap_out: pcap_out.map(Into::into),
|
||||
duration_secs: seconds,
|
||||
};
|
||||
state
|
||||
.0
|
||||
.start_scan_with(&iface, options)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_check(state: tauri::State<'_, State>) -> Result<serde_json::Value, String> {
|
||||
let status = state.0.update_check().map_err(|e| e.to_string())?;
|
||||
@@ -48,9 +506,9 @@ fn update_apply(state: tauri::State<'_, State>) -> Result<String, String> {
|
||||
state.0.update_apply().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// `true` quando o processo já tem privilégio suficiente pra tudo que o
|
||||
/// Sabiá faz (modo monitor, captura crua). No Windows o manifesto já
|
||||
/// exige administrador antes mesmo do app abrir, então aqui é sempre `true`.
|
||||
/// `true` when the process already has sufficient privileges for all
|
||||
/// Sabiá operations (monitor mode, raw capture). On Windows the manifest already
|
||||
/// requires administrator privileges before launch, so this is always `true`.
|
||||
#[tauri::command]
|
||||
fn is_elevated() -> bool {
|
||||
#[cfg(unix)]
|
||||
@@ -63,9 +521,9 @@ fn is_elevated() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Reabre o app com privilégios de administrador (Linux/macOS). O app novo
|
||||
/// encerra este quando abre; se a autorização falhar ou for cancelada, a
|
||||
/// janela continua aqui e a interface mostra o motivo.
|
||||
/// Relaunches the app with administrator privileges (Linux/macOS). The new instance
|
||||
/// terminates this one on startup; if authorization fails or is cancelled, the
|
||||
/// window stays open and the UI displays the reason.
|
||||
#[tauri::command]
|
||||
fn relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
@@ -85,7 +543,10 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
let exe = elevated_exe()?;
|
||||
let log = std::env::temp_dir().join(format!("sabia-elevate-{}.log", std::process::id()));
|
||||
let _ = std::fs::write(&log, b"");
|
||||
let out = std::fs::OpenOptions::new().append(true).open(&log).map_err(|e| e.to_string())?;
|
||||
let out = std::fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&log)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let err = out.try_clone().map_err(|e| e.to_string())?;
|
||||
let pid = std::process::id().to_string();
|
||||
|
||||
@@ -93,7 +554,14 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
let mut cmd = {
|
||||
let mut c = std::process::Command::new("pkexec");
|
||||
c.arg("/usr/bin/env");
|
||||
for k in ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR", "DBUS_SESSION_BUS_ADDRESS", "GDK_BACKEND"] {
|
||||
for k in [
|
||||
"DISPLAY",
|
||||
"WAYLAND_DISPLAY",
|
||||
"XAUTHORITY",
|
||||
"XDG_RUNTIME_DIR",
|
||||
"DBUS_SESSION_BUS_ADDRESS",
|
||||
"GDK_BACKEND",
|
||||
] {
|
||||
if let Ok(v) = std::env::var(k) {
|
||||
c.arg(format!("{k}={v}"));
|
||||
}
|
||||
@@ -104,14 +572,23 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
let mut cmd = {
|
||||
let shell = format!("SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &", pid, exe.display());
|
||||
let script = format!("do shell script \"{}\" with administrator privileges", shell.replace('\\', "\\\\").replace('"', "\\\""));
|
||||
let shell = format!(
|
||||
"SABIA_REPLACE_PID={} '{}' >/dev/null 2>&1 &",
|
||||
pid,
|
||||
exe.display()
|
||||
);
|
||||
let script = format!(
|
||||
"do shell script \"{}\" with administrator privileges",
|
||||
shell.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
);
|
||||
let mut c = std::process::Command::new("osascript");
|
||||
c.args(["-e", &script]);
|
||||
c
|
||||
};
|
||||
cmd.stdout(out).stderr(err);
|
||||
let mut child = cmd.spawn().map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|e| format!("não foi possível pedir privilégios: {e}"))?;
|
||||
std::thread::spawn(move || {
|
||||
let status = child.wait();
|
||||
if matches!(&status, Ok(s) if s.success()) {
|
||||
@@ -122,7 +599,10 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
let msg = match status.ok().and_then(|s| s.code()) {
|
||||
Some(126) => "Autorização cancelada.".to_string(),
|
||||
Some(127) => "Não foi possível autenticar como administrador.".to_string(),
|
||||
_ => format!("Não foi possível reiniciar como administrador. {}", detail.lines().last().unwrap_or("").trim()),
|
||||
_ => format!(
|
||||
"Não foi possível reiniciar como administrador. {}",
|
||||
detail.lines().last().unwrap_or("").trim()
|
||||
),
|
||||
};
|
||||
let _ = app.emit("elevate-failed", msg);
|
||||
let _ = std::fs::remove_file(&log);
|
||||
@@ -130,12 +610,15 @@ fn do_relaunch_elevated(app: tauri::AppHandle) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Caminho que o root consegue executar. No AppImage, `current_exe` aponta
|
||||
/// pra dentro de um ponto de montagem FUSE do usuário, inacessível ao root:
|
||||
/// usa o próprio arquivo `.AppImage` (variável `APPIMAGE`).
|
||||
/// Executable path accessible to root. Inside an AppImage, `current_exe` points
|
||||
/// inside a user FUSE mount that root cannot access:
|
||||
/// use the `.AppImage` file itself (`APPIMAGE` environment variable).
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn elevated_exe() -> Result<std::path::PathBuf, String> {
|
||||
if let Some(p) = std::env::var_os("APPIMAGE").map(std::path::PathBuf::from).filter(|p| p.is_file()) {
|
||||
if let Some(p) = std::env::var_os("APPIMAGE")
|
||||
.map(std::path::PathBuf::from)
|
||||
.filter(|p| p.is_file())
|
||||
{
|
||||
return Ok(p);
|
||||
}
|
||||
std::env::current_exe().map_err(|e| e.to_string())
|
||||
@@ -145,13 +628,19 @@ fn main() {
|
||||
let engine = Arc::new(Engine::new());
|
||||
tauri::Builder::default()
|
||||
.manage(State(engine))
|
||||
.setup(|app| {
|
||||
// Quando abrimos elevados a pedido do app normal, ele fecha o app antigo.
|
||||
.setup(|_app| {
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
let app = _app;
|
||||
|
||||
// When launched elevated by the unprivileged instance, terminate the old process.
|
||||
#[cfg(unix)]
|
||||
if let Some(pid) = std::env::var("SABIA_REPLACE_PID").ok().and_then(|p| p.parse::<i32>().ok()) {
|
||||
if let Some(pid) = std::env::var("SABIA_REPLACE_PID")
|
||||
.ok()
|
||||
.and_then(|p| p.parse::<i32>().ok())
|
||||
{
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(std::time::Duration::from_millis(1200));
|
||||
// SAFETY: só envia SIGTERM ao processo anterior do próprio usuário.
|
||||
// SAFETY: only sends SIGTERM to the previous process owned by the invoking user.
|
||||
unsafe {
|
||||
if libc::geteuid() == 0 {
|
||||
libc::kill(pid, libc::SIGTERM);
|
||||
@@ -160,10 +649,10 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
// Captura crua e modo monitor exigem root: pede elevação assim
|
||||
// que o app abre, sem esperar o usuário procurar um botão. Se
|
||||
// a autorização falhar, a janela continua aberta e a interface
|
||||
// mostra o aviso (com um botão pra tentar de novo).
|
||||
// Raw capture and monitor mode require root: request elevation as soon
|
||||
// as the app launches, without waiting for the user to look for a button. If
|
||||
// authorization fails, the window stays open and the UI
|
||||
// displays a warning banner (with a retry button).
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
if unsafe { libc::geteuid() != 0 } && std::env::var("SABIA_REPLACE_PID").is_err() {
|
||||
let handle = app.handle().clone();
|
||||
@@ -180,6 +669,42 @@ fn main() {
|
||||
start_scan,
|
||||
stop_scan,
|
||||
scan_results,
|
||||
scan_snapshot,
|
||||
scan_clients,
|
||||
handshakes,
|
||||
export_handshake,
|
||||
export_all_handshakes,
|
||||
start_crack,
|
||||
start_crack_mask,
|
||||
crack_job,
|
||||
crack_jobs,
|
||||
deauth,
|
||||
deauth_directional,
|
||||
start_deauth_job,
|
||||
stop_deauth_job,
|
||||
deauth_job,
|
||||
deauth_jobs,
|
||||
capture_handshake,
|
||||
report,
|
||||
report_html,
|
||||
export_hc22000,
|
||||
export_all_hc22000,
|
||||
pmkid_hunt,
|
||||
pmf_test,
|
||||
wps_tools,
|
||||
start_wps,
|
||||
wps_job,
|
||||
wps_jobs,
|
||||
stop_wps,
|
||||
start_evil_twin,
|
||||
eviltwin_job,
|
||||
eviltwin_jobs,
|
||||
stop_evil_twin,
|
||||
start_evil_twin_enterprise,
|
||||
eviltwin_enterprise_job,
|
||||
eviltwin_enterprise_jobs,
|
||||
stop_evil_twin_enterprise,
|
||||
start_scan_opts,
|
||||
update_check,
|
||||
update_apply,
|
||||
is_elevated,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Sabia",
|
||||
"version": "0.1.0",
|
||||
"productName": "Sabiá",
|
||||
"version": "1.0.3",
|
||||
"identifier": "dev.sabia.app",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
@@ -19,7 +19,7 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; frame-src 'none'; object-src 'none'"
|
||||
"csp": "default-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self'; worker-src 'self'; frame-src 'self' data: blob:; object-src 'none'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
--text-3: #8e8e93;
|
||||
--line: rgba(0, 0, 0, 0.09);
|
||||
|
||||
/* "canto" — cobre quente, o peito do sabiá-laranjeira */
|
||||
/* "song" — warm copper, the rufous-bellied thrush breast */
|
||||
--accent: #b45309;
|
||||
--accent-ink: #ffffff;
|
||||
--accent-soft: rgba(180, 83, 9, 0.11);
|
||||
/* "sinal" — ciano, a onda que o rádio enxerga */
|
||||
/* "signal" — cyan, the radio wave */
|
||||
--signal: #0891b2;
|
||||
--signal-soft: rgba(8, 145, 178, 0.12);
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
:root[data-theme="dark"] { color-scheme: dark; }
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
[hidden] { display: none !important; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--text);
|
||||
@@ -104,23 +105,33 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
padding: 9px 10px; border-radius: 12px; transition: background .15s;
|
||||
}
|
||||
.dev:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
|
||||
.dev[aria-current="true"] { background: var(--accent-soft); }
|
||||
.dev[aria-current="true"], .dev[aria-pressed="true"] { background: var(--accent-soft); }
|
||||
.dev .ico { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; background: var(--surface); box-shadow: 0 0 0 1px var(--line); color: var(--text-2); }
|
||||
.dev[aria-current="true"] .ico { color: var(--accent); }
|
||||
.dev[aria-current="true"] .ico, .dev[aria-pressed="true"] .ico { color: var(--accent); }
|
||||
.dev .t { display: block; font-weight: 600; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.dev .s { display: block; font-size: 11.5px; color: var(--text-2); font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.side-foot { margin-top: auto; padding: 14px 10px 4px; font-size: 11.5px; color: var(--text-3); display: flex; justify-content: space-between; gap: 8px; }
|
||||
.side-foot button { background: transparent; border: 0; color: var(--text-3); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
||||
.iface-pick { display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding: 0 10px 4px; }
|
||||
.iface-pick .select { width: 100%; min-width: 0; }
|
||||
.iface-pick .pill { align-self: flex-start; }
|
||||
|
||||
.side-foot { margin-top: auto; padding: 14px 4px 4px; font-size: 11.5px; color: var(--text-3); display: grid; gap: 8px; }
|
||||
.foot-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.side-foot button { background: transparent; border: 0; color: var(--text-3); cursor: pointer; }
|
||||
.side-foot .foot-row > button:not(.icon-btn) { text-decoration: underline; text-underline-offset: 2px; }
|
||||
.side-foot button:hover { color: var(--text-2); }
|
||||
.lang-sel { background: transparent; border: 0; color: var(--text-3); font-size: 11.5px; cursor: pointer; }
|
||||
.lang-sel:hover { color: var(--text-2); }
|
||||
.icon-btn { width: 30px; height: 30px; border-radius: 9px; display: inline-grid; place-items: center; padding: 0; }
|
||||
.icon-btn:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
|
||||
|
||||
.theme-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); }
|
||||
.theme-switch button { border: 0; background: transparent; cursor: pointer; padding: 4px 9px; border-radius: 999px; font-size: 11.5px; color: var(--text-2); }
|
||||
.theme-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .18); font-weight: 600; }
|
||||
|
||||
.main { overflow-y: auto; min-width: 0; }
|
||||
.page { max-width: 1040px; margin: 0 auto; padding: 30px 36px 100px; animation: rise .35s cubic-bezier(.2, .8, .2, 1); }
|
||||
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* -------------------------------------------------------- topo + linha de sinal */
|
||||
/* -------------------------------------------------------- topbar + signal line */
|
||||
.topbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.topbar h1 { font-size: 21px; }
|
||||
.topbar .sub { color: var(--text-2); font-size: 13px; margin-top: 2px; }
|
||||
@@ -130,11 +141,14 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.wave.scanning path.live { opacity: 1; }
|
||||
.wave.scanning path.flat { opacity: 0; }
|
||||
|
||||
/* ------------------------------------------------------------ aviso */
|
||||
/* ------------------------------------------------------------ notice banner */
|
||||
.notice { display: flex; gap: 10px; background: var(--surface); border-radius: 12px; padding: 12px 14px; box-shadow: var(--shadow); font-size: 13px; color: var(--text-2); margin-bottom: 20px; align-items: flex-start; }
|
||||
.notice svg { flex: none; margin-top: 2px; color: var(--warn); }
|
||||
.notice.warn { background: var(--warn-soft); } .notice.warn svg { color: var(--warn); }
|
||||
.notice.bad { background: var(--bad-soft); } .notice.bad svg { color: var(--bad); }
|
||||
.notice b { color: var(--text); }
|
||||
|
||||
/* ------------------------------------------------------------ controles */
|
||||
/* ------------------------------------------------------------ controls */
|
||||
.controls { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); padding: 16px 18px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 22px; }
|
||||
.controls .group { display: flex; align-items: center; gap: 8px; }
|
||||
.pill { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); }
|
||||
@@ -152,24 +166,58 @@ code, .mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
||||
.btn.live { background: var(--bad); color: #fff; }
|
||||
.btn.live:hover { filter: brightness(1.06); }
|
||||
|
||||
/* ------------------------------------------------------------ tabela */
|
||||
.result-head { display: flex; align-items: baseline; justify-content: space-between; margin: 4px 2px 10px; }
|
||||
/* ------------------------------------------------------------ table */
|
||||
.result-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 4px 2px 10px; }
|
||||
.result-summary { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
|
||||
.result-head strong { font-size: 15px; }
|
||||
.result-head span { color: var(--text-3); font-size: 12.5px; }
|
||||
.result-summary span { color: var(--text-3); font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.result-tools { display: flex; align-items: center; gap: 8px; }
|
||||
.result-tools .filter { border: 0; border-radius: 10px; padding: 7px 12px; width: 210px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; font-size: 12.5px; }
|
||||
.result-tools .filter::placeholder { color: var(--text-3); }
|
||||
.result-tools .select { width: auto; padding: 7px 10px; border-radius: 10px; font-size: 12.5px; }
|
||||
|
||||
.ap-card { background: var(--surface); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
|
||||
.net-card { overflow-x: auto; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
thead th { text-align: left; padding: 11px 14px; font-size: 10.5px; color: var(--text-3); text-transform: uppercase; letter-spacing: .06em; font-weight: 600; border-bottom: 1px solid var(--line); }
|
||||
tbody td { padding: 10px 14px; font-size: 13px; border-bottom: 1px solid var(--line); vertical-align: middle; }
|
||||
tbody tr:last-child td { border-bottom: 0; }
|
||||
tbody tr:hover { background: var(--surface-2); }
|
||||
td.bssid { font-family: var(--mono); font-size: 12px; color: var(--text-2); }
|
||||
td.ssid { font-weight: 600; }
|
||||
td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
|
||||
.sec-tag { font-size: 11.5px; font-weight: 600; padding: 2px 8px; border-radius: 999px; }
|
||||
.sec-aberta { background: var(--bad-soft); color: var(--bad); }
|
||||
.sec-wep { background: var(--warn-soft); color: var(--warn); }
|
||||
.sec-wpa, .sec-wpa2, .sec-wpa3 { background: var(--ok-soft); color: var(--ok); }
|
||||
|
||||
/* network grouped by ESSID; each BSSID renders as an expandable row */
|
||||
.caret-cell { width: 34px; padding-left: 10px; padding-right: 0; }
|
||||
.expander { width: 26px; height: 26px; border: 0; border-radius: 8px; background: transparent; color: var(--text-3); cursor: pointer; display: inline-grid; place-items: center; padding: 0; }
|
||||
.expander:hover { background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text); }
|
||||
.expander svg { transition: transform .18s; }
|
||||
.net-row.open .expander svg { transform: rotate(90deg); }
|
||||
.net-row.open > td { background: color-mix(in srgb, var(--text) 3%, transparent); }
|
||||
.net-name .ssid { display: block; font-weight: 600; line-height: 1.25; }
|
||||
.net-name .ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
|
||||
.net-sub { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 3px; }
|
||||
.flag { font-size: 10px; font-weight: 700; letter-spacing: .03em; padding: 1px 6px; border-radius: 999px; background: color-mix(in srgb, var(--text) 8%, transparent); color: var(--text-2); white-space: nowrap; }
|
||||
.flag-warn { background: var(--warn-soft); color: var(--warn); }
|
||||
.net-sec .sec-tag + .sec-tag { margin-left: 4px; }
|
||||
.net-chan .chans { font-variant-numeric: tabular-nums; font-weight: 600; }
|
||||
.net-band { display: block; font-size: 10.5px; color: var(--text-3); }
|
||||
.net-signal, .net-clients { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.net-clients { color: var(--text-2); }
|
||||
|
||||
.detail-row td { padding: 4px 14px 10px 46px; background: var(--surface-2); }
|
||||
.detail-row:hover { background: transparent; }
|
||||
.bssid-list { display: grid; gap: 1px; }
|
||||
.bssid-item { display: grid; grid-template-columns: minmax(150px, 1.4fr) 132px 112px minmax(92px, .8fr) 84px 1fr; gap: 10px; align-items: center; padding: 6px 8px; border-radius: 8px; font-size: 12.5px; }
|
||||
.bssid-item:hover { background: color-mix(in srgb, var(--text) 4%, transparent); }
|
||||
.bssid { color: var(--text-2); font-size: 11.5px; }
|
||||
.bssid-meta, .bssid-clients { color: var(--text-3); font-size: 12px; }
|
||||
.bssid-flags { display: flex; gap: 4px; justify-content: flex-end; flex-wrap: wrap; }
|
||||
|
||||
.sec-tag { font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 999px; white-space: nowrap; }
|
||||
.sec-open { background: var(--bad-soft); color: var(--bad); }
|
||||
.sec-wep, .sec-wpa { background: var(--warn-soft); color: var(--warn); }
|
||||
.sec-wpa2, .sec-wpa3, .sec-owe { background: var(--ok-soft); color: var(--ok); }
|
||||
.sec-eap { background: var(--signal-soft); color: var(--signal); }
|
||||
.signal-bars { display: inline-flex; align-items: flex-end; gap: 1.5px; height: 14px; vertical-align: middle; margin-right: 6px; }
|
||||
.signal-bars i { width: 3px; background: var(--line); border-radius: 1px; display: block; }
|
||||
.signal-bars i.on { background: var(--signal); }
|
||||
@@ -179,11 +227,233 @@ td.ssid em { color: var(--text-3); font-weight: 400; font-style: normal; }
|
||||
.empty-state h2 { font-size: 17px; color: var(--text); margin-bottom: 6px; }
|
||||
.empty-state p { font-size: 13.5px; max-width: 360px; margin: 0 auto; }
|
||||
|
||||
/* ------------------------------------------------------------ responsivo */
|
||||
/* ------------------------------------------------------------ settings */
|
||||
dialog { border: 0; border-radius: 20px; padding: 24px; width: min(420px, calc(100vw - 32px)); background: var(--surface); color: var(--text); box-shadow: 0 30px 80px rgba(0, 0, 0, .35); }
|
||||
dialog::backdrop { background: rgba(0, 0, 0, .35); backdrop-filter: blur(6px); }
|
||||
dialog h2 { font-size: 20px; margin-bottom: 16px; }
|
||||
dialog menu { display: flex; gap: 10px; justify-content: flex-end; padding: 0; margin: 22px 0 0; }
|
||||
.field { display: grid; gap: 6px; margin: 12px 0; }
|
||||
.field-label { font-size: 12px; color: var(--text-2); font-weight: 600; }
|
||||
.select { width: 100%; border: 0; border-radius: 12px; padding: 9px 14px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; cursor: pointer; }
|
||||
.select option { background-color: var(--surface); color: var(--text); }
|
||||
|
||||
|
||||
/* ------------------------------------------------------------ target details */
|
||||
.ssid-btn { display: block; border: 0; padding: 0; background: transparent; color: inherit; font: inherit; font-weight: 600; text-align: left; cursor: pointer; line-height: 1.25; }
|
||||
.ssid-btn:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
||||
.bssid-item { cursor: pointer; }
|
||||
.bssid-item:focus-visible { outline: 2px solid var(--signal); outline-offset: -2px; }
|
||||
.target-dlg { width: min(980px, calc(100vw - 32px)); max-height: min(90vh, 900px); padding: 0; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.target-dlg:not([open]) { display: none; }
|
||||
.target-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 22px 24px 12px; }
|
||||
.target-head h2 { font-size: 22px; line-height: 1.2; }
|
||||
.target-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 8px; color: var(--text-2); font-size: 12.5px; margin-top: 5px; }
|
||||
.target-sub .mono { color: var(--text-2); font-size: 12px; }
|
||||
.target-sub .sec-tag { margin-left: 2px; }
|
||||
.dot-sep { color: var(--text-3); }
|
||||
.bssid-select { width: auto; max-width: 320px; padding: 4px 8px; border-radius: 8px; font-size: 11.5px; }
|
||||
.tabs { display: flex; gap: 2px; padding: 0 20px; border-bottom: 1px solid var(--line); }
|
||||
.tabs button { border: 0; background: transparent; color: var(--text-2); padding: 9px 12px 11px; font-size: 13px; font-weight: 600; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
||||
.tabs button:hover { color: var(--text); }
|
||||
.tabs button[aria-selected="true"] { color: var(--text); border-bottom-color: var(--accent); }
|
||||
.tab-count { display: inline-block; min-width: 18px; padding: 0 5px; margin-left: 3px; border-radius: 999px; background: color-mix(in srgb, var(--text) 9%, transparent); color: var(--text-2); font-size: 10.5px; line-height: 17px; text-align: center; }
|
||||
.tab-panel { padding: 18px 24px 26px; overflow-y: auto; min-height: 0; }
|
||||
.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.detail-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; }
|
||||
.detail-card h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); margin-bottom: 10px; }
|
||||
.detail-wide { margin-top: 12px; }
|
||||
.kv { display: grid; grid-template-columns: minmax(96px, auto) minmax(0, 1fr); gap: 6px 12px; margin: 0; font-size: 12.5px; }
|
||||
.kv dt { color: var(--text-3); }
|
||||
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
.detail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
.btn.small { padding: 6px 12px; font-size: 12px; }
|
||||
.risk { border-radius: 12px; padding: 10px 13px; font-size: 12.5px; margin-bottom: 12px; border: 1px solid transparent; }
|
||||
.risk-bad { background: var(--bad-soft); color: var(--bad); }
|
||||
.risk-warn { background: var(--warn-soft); color: var(--warn); }
|
||||
.risk-info { background: var(--signal-soft); color: var(--signal); }
|
||||
.risk strong { font-weight: 700; }
|
||||
.flag-ok { background: var(--ok-soft); color: var(--ok); }
|
||||
.flag-bad { background: var(--bad-soft); color: var(--bad); }
|
||||
.clients-card { margin-top: 2px; overflow-x: auto; }
|
||||
.mini-table { width: 100%; border-collapse: collapse; }
|
||||
.mini-table .row-actions { text-align: right; white-space: nowrap; }
|
||||
.mini-table th { padding: 9px 12px; font-size: 10.5px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-3); text-align: left; border-bottom: 1px solid var(--line); }
|
||||
.mini-table td { padding: 9px 12px; font-size: 12.5px; border-bottom: 1px solid var(--line); vertical-align: middle; }
|
||||
.mini-table tr:last-child td { border-bottom: 0; }
|
||||
.mini-table .nowrap { white-space: nowrap; font-variant-numeric: tabular-nums; }
|
||||
.mini-table .probes { max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
|
||||
.mini-table .device-cell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
|
||||
.clients-card .mini-table { table-layout: fixed; }
|
||||
.clients-card .mini-table th:nth-child(1) { width: 20%; }
|
||||
.clients-card .mini-table th:nth-child(2) { width: 18%; }
|
||||
.clients-card .mini-table th:nth-child(3) { width: 12%; }
|
||||
.clients-card .mini-table th:nth-child(4) { width: 7%; }
|
||||
.clients-card .mini-table th:nth-child(5) { width: 10%; }
|
||||
.clients-card .mini-table th:nth-child(6) { width: 12%; }
|
||||
.clients-card .mini-table th:nth-child(7) { width: 21%; }
|
||||
.clients-card .row-actions .btn { padding: 6px 10px; }
|
||||
.clients-card .client-name, .clients-card .client-sub { max-width: 100%; }
|
||||
.client-name { font-weight: 600; color: var(--text); max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.client-sub { color: var(--text-3); font-size: 10.5px; max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.empty-inline { text-align: center; padding: 46px 20px; color: var(--text-2); background: var(--surface-2); border-radius: 14px; }
|
||||
.empty-inline strong { color: var(--text); display: block; margin-bottom: 4px; }
|
||||
.empty-inline p { margin: 0; font-size: 13px; }
|
||||
.auth-check { display: flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 600; margin: 4px 0 14px; }
|
||||
.auth-check input { width: 16px; height: 16px; accent-color: var(--accent); }
|
||||
/* CSS multi-column layout instead of grid: attack cards have very different
|
||||
heights (Deauth/Handshake are tall, PMF/WPS are short). In a standard
|
||||
2-column grid, the shorter card in a row left a large empty gap underneath.
|
||||
Multi-column masonry flow lets each card take only the height it needs
|
||||
with the next card stacking right below. */
|
||||
.attack-grid { column-count: 2; column-gap: 12px; }
|
||||
.attack-card { background: var(--surface-2); border: 1px solid var(--line); border-radius: 14px; padding: 14px 16px; min-width: 0; break-inside: avoid; margin: 0 0 12px; display: inline-block; width: 100%; }
|
||||
.attack-deauth, .attack-wide { column-span: all; }
|
||||
.attack-deauth .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.attack-deauth .detail-actions { align-items: center; }
|
||||
.attack-card h3 { font-size: 14px; margin-bottom: 5px; }
|
||||
.attack-card .hint { margin: 6px 0 10px; }
|
||||
.attack-card .btn { margin-top: 2px; }
|
||||
.field-row { display: grid; grid-template-columns: 1fr 1.4fr; gap: 10px; }
|
||||
.field { display: grid; gap: 5px; margin: 9px 0; }
|
||||
.field-label { font-size: 11.5px; color: var(--text-2); font-weight: 600; }
|
||||
.input { width: 100%; border: 1px solid transparent; border-radius: 10px; padding: 8px 11px; background: color-mix(in srgb, var(--text) 7%, transparent); color: var(--text); font: inherit; font-size: 12.5px; }
|
||||
.input:focus { outline: 2px solid var(--signal); outline-offset: 1px; }
|
||||
.hint { color: var(--text-3); font-size: 12px; margin: 0; line-height: 1.4; }
|
||||
.hint.bad { color: var(--bad); }
|
||||
.hint code { background: color-mix(in srgb, var(--text) 7%, transparent); padding: 1px 5px; border-radius: 5px; font-size: 11.5px; }
|
||||
.hs-list { display: grid; gap: 6px; margin-bottom: 8px; }
|
||||
.hs-item { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; background: var(--surface); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; font-size: 12px; }
|
||||
.hs-main { min-width: 0; display: grid; gap: 2px; }
|
||||
.hs-meta { color: var(--text-3); font-size: 11px; }
|
||||
.hs-flags { display: flex; flex-wrap: wrap; gap: 4px; justify-content: flex-end; }
|
||||
.crack-box { margin-top: 10px; padding: 9px 11px; border-radius: 10px; background: var(--surface); border: 1px solid var(--line); font-size: 12px; display: flex; flex-wrap: wrap; align-items: center; gap: 7px; }
|
||||
.crack-ok { border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
|
||||
.crack-box .mono { margin-right: 4px; }
|
||||
.crack-head { display: flex; align-items: center; gap: 8px; width: 100%; color: var(--text-2); }
|
||||
.crack-bar { width: 100%; height: 4px; border-radius: 999px; background: var(--line); overflow: hidden; }
|
||||
.crack-bar i { display: block; width: 35%; height: 100%; border-radius: 999px; background: var(--accent); animation: crack-slide 1.1s ease-in-out infinite; }
|
||||
@keyframes crack-slide { 0% { margin-left: -35%; } 100% { margin-left: 100%; } }
|
||||
|
||||
/* ------------------------------------------------------------ responsive */
|
||||
@media (max-width: 760px) {
|
||||
.app { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
|
||||
.sidebar { border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; align-items: center; overflow-x: auto; }
|
||||
.brand { padding: 4px 10px; }
|
||||
.side-title, .side-foot { display: none; }
|
||||
.list { display: flex; }
|
||||
.result-tools { width: 100%; }
|
||||
.result-tools .filter { flex: 1; width: auto; }
|
||||
.net-table { min-width: 560px; }
|
||||
.bssid-item { grid-template-columns: 1fr auto; }
|
||||
.bssid-meta, .bssid-sig, .bssid-flags { grid-column: 1 / -1; }
|
||||
.bssid-flags { justify-content: flex-start; }
|
||||
.target-dlg { width: calc(100vw - 16px); max-height: 94vh; }
|
||||
.target-head { padding: 16px 16px 10px; }
|
||||
.tabs { padding: 0 10px; overflow-x: auto; }
|
||||
.tab-panel { padding: 14px 14px 20px; }
|
||||
.detail-grid { grid-template-columns: 1fr; }
|
||||
.attack-grid { column-count: 1; }
|
||||
.field-row { grid-template-columns: 1fr; }
|
||||
.detail-card, .attack-card { padding: 12px; }
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- channel analysis */
|
||||
.top-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.view-switch { display: inline-flex; padding: 2px; border-radius: 999px; background: color-mix(in srgb, var(--text) 7%, transparent); }
|
||||
.view-switch button { border: 0; border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; color: var(--text-2); background: transparent; cursor: pointer; }
|
||||
.view-switch button[aria-pressed="true"] { background: var(--surface); color: var(--text); box-shadow: 0 1px 3px rgba(0, 0, 0, .12); }
|
||||
.alert-strip { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 0 2px 10px; }
|
||||
.alert-item { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); background: var(--surface); border-radius: 999px; padding: 5px 11px; font-size: 12px; cursor: pointer; }
|
||||
.alert-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--warn); }
|
||||
.alert-high .alert-dot { background: var(--bad); }
|
||||
.alert-low .alert-dot { background: var(--text-3); }
|
||||
.alert-title { max-width: 340px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.alert-more { color: var(--text-3); font-size: 11.5px; padding: 0 4px; }
|
||||
.channel-card { padding: 16px; }
|
||||
.channel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.band-switch { display: inline-flex; gap: 4px; }
|
||||
.band-switch button { border: 1px solid transparent; background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text-2); border-radius: 999px; padding: 5px 12px; font-size: 12px; font-weight: 600; cursor: pointer; }
|
||||
.band-switch button[aria-pressed="true"] { background: var(--signal-soft); color: var(--signal); border-color: color-mix(in srgb, var(--signal) 35%, transparent); }
|
||||
.sp-legend { display: flex; flex-wrap: wrap; gap: 10px; color: var(--text-3); font-size: 11px; }
|
||||
.sp-legend span { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.sp-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
|
||||
.spectrum { width: 100%; height: auto; display: block; background: color-mix(in srgb, var(--text) 3%, transparent); border-radius: 12px; }
|
||||
.sp-grid { stroke: var(--line); stroke-width: 1; }
|
||||
.sp-axis { stroke: color-mix(in srgb, var(--text) 22%, transparent); stroke-width: 1; }
|
||||
.sp-db, .sp-chan { fill: var(--text-3); font-size: 9px; font-family: var(--mono); }
|
||||
.sp-name { fill: var(--text-2); font-size: 9px; font-weight: 600; }
|
||||
.sp-lobe path { transition: fill-opacity .15s; }
|
||||
.sp-lobe:hover path { fill-opacity: .38; }
|
||||
.channel-grid { display: grid; grid-template-columns: 190px 1fr; gap: 18px; margin-top: 14px; }
|
||||
.channel-grid h3 { font-size: 12px; color: var(--text-2); margin-bottom: 8px; }
|
||||
.chan-best-num { font-size: 44px; font-weight: 700; line-height: 1; color: var(--signal); margin: 0 0 6px; }
|
||||
.chan-list { display: grid; gap: 6px; }
|
||||
.chan-list h3 { margin: 0 0 2px; }
|
||||
.chan-row { display: grid; grid-template-columns: 46px 1fr 48px 128px; align-items: center; gap: 8px; font-size: 12px; }
|
||||
.chan-row .chan-n { font-weight: 600; display: inline-flex; align-items: center; gap: 4px; }
|
||||
.chan-bar { height: 7px; border-radius: 999px; background: var(--line); overflow: hidden; }
|
||||
.chan-bar i { display: block; height: 100%; border-radius: 999px; background: var(--ok); }
|
||||
.chan-bar i.diff-mid { background: var(--warn); }
|
||||
.chan-bar i.diff-high { background: var(--bad); }
|
||||
.chan-score { font-family: var(--mono); font-size: 11px; color: var(--text-2); text-align: right; }
|
||||
.chan-aps { color: var(--text-3); font-size: 11px; white-space: nowrap; }
|
||||
.chan-best .chan-n, .chan-best .chan-aps { color: var(--signal); }
|
||||
|
||||
/* ------------------------------------------------- spectrum health (SMART) */
|
||||
.chan-tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 10px; margin: 2px 0 16px; }
|
||||
.chan-tile { background: var(--surface-2); border-radius: 14px; padding: 12px 14px; display: flex; flex-direction: column; gap: 2px; box-shadow: 0 0 0 1px var(--line); }
|
||||
.chan-tile small { color: var(--text-3); font-size: 12px; }
|
||||
.chan-tile b { font-size: 18px; font-variant-numeric: tabular-nums; }
|
||||
.chan-tile.status.ok { background: var(--ok-soft); } .chan-tile.status.ok b { color: var(--ok); }
|
||||
.chan-tile.status.warn { background: var(--warn-soft); } .chan-tile.status.warn b { color: var(--warn); }
|
||||
.chan-tile.status.bad { background: var(--bad-soft); } .chan-tile.status.bad b { color: var(--bad); }
|
||||
.chan-tile b span { font-weight: 500; opacity: .8; font-size: 13px; margin-left: 4px; }
|
||||
|
||||
.chan-dot { display: inline-block; width: 9px; height: 9px; border-radius: 50%; background: var(--ok); margin-right: 5px; flex: none; }
|
||||
.chan-dot.warn { background: var(--warn); }
|
||||
.chan-dot.bad { background: var(--bad); }
|
||||
|
||||
.chan-attrs { margin-top: 4px; }
|
||||
.chan-attrs summary { cursor: pointer; font-size: 12.5px; font-weight: 600; color: var(--text-2); padding: 6px 0; list-style: none; }
|
||||
.chan-attrs summary::-webkit-details-marker { display: none; }
|
||||
.chan-attrs summary::before { content: '▸ '; display: inline-block; width: 12px; }
|
||||
.chan-attrs[open] summary::before { content: '▾ '; }
|
||||
.chan-table .dim { color: var(--text-3); font-size: 11.5px; }
|
||||
.chan-table tr.chan-best-row td { color: var(--signal); font-weight: 600; }
|
||||
.mini-tag { font-size: 8.5px; padding: 1px 4px; border-radius: 4px; background: var(--warn-soft); color: var(--warn); }
|
||||
|
||||
/* ------------------------------------------------ clients and timeline */
|
||||
.spark { display: inline-block; width: 62px; height: 16px; vertical-align: middle; margin-left: 6px; }
|
||||
.spark path { fill: none; stroke: var(--signal); stroke-width: 1.4; }
|
||||
.spark-big { display: block; width: 100%; height: 70px; margin: 10px 0 0; }
|
||||
.timeline-card { margin-top: 12px; }
|
||||
.timeline-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 6px; }
|
||||
.timeline { list-style: none; padding: 0; margin: 8px 0; display: grid; gap: 5px; max-height: 260px; overflow: auto; }
|
||||
.timeline li { display: grid; grid-template-columns: 84px 112px 1fr; align-items: baseline; gap: 8px; font-size: 12px; border-bottom: 1px solid var(--line); padding: 5px 2px; }
|
||||
.timeline li > .mono:first-child { white-space: nowrap; }
|
||||
.tl-kind { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .03em; color: var(--text-3); }
|
||||
.tl-deauth { color: var(--bad); }
|
||||
.tl-associate { color: var(--ok); }
|
||||
.tl-probe { color: var(--signal); }
|
||||
.tl-detail { color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ------------------------------------------------------------- WPS and report */
|
||||
.term { margin: 10px 0 0; padding: 10px 12px; border-radius: 10px; background: color-mix(in srgb, var(--text) 6%, transparent); color: var(--text-2); font: 11.5px/1.5 var(--mono); white-space: pre-wrap; max-height: 180px; overflow: auto; }
|
||||
.report-dlg { width: min(1060px, 96vw); height: 92vh; max-height: 92vh; padding: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.report-dlg:not([open]) { display: none; }
|
||||
.report-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: 16px 18px 12px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
||||
.report-head h2 { font-size: 17px; margin: 0 0 2px; }
|
||||
.report-tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
||||
.report-tools .mono { font-size: 11.5px; color: var(--text-2); min-width: 38px; text-align: center; }
|
||||
.report-view { flex: 1; overflow: auto; background: color-mix(in srgb, var(--text) 8%, var(--bg)); padding: 16px; }
|
||||
.report-canvas { margin: 0 auto; transform-origin: top center; background: #fff; box-shadow: 0 6px 30px rgba(0, 0, 0, .18); }
|
||||
.report-canvas iframe { border: 0; display: block; background: #fff; }
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.top-actions { gap: 6px; }
|
||||
.channel-grid { grid-template-columns: 1fr; }
|
||||
.chan-row { grid-template-columns: 42px 1fr 44px; }
|
||||
.chan-aps { grid-column: 2 / -1; }
|
||||
.report-dlg { width: 100vw; height: 100vh; max-height: 100vh; border-radius: 0; }
|
||||
.report-view { padding: 8px; }
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
// Dicionário português -> inglês da interface. {n} marca um valor variável.
|
||||
// Portuguese -> English UI dictionary. {n} marks a variable placeholder.
|
||||
export const EN = {
|
||||
// index.html / sidebar
|
||||
'auditoria de Wi-Fi': 'Wi-Fi auditing',
|
||||
'Interface': 'Interface',
|
||||
'Interface Wi-Fi': 'Wi-Fi interface',
|
||||
'Interfaces': 'Interfaces',
|
||||
'nenhuma interface Wi-Fi encontrada': 'no Wi-Fi interface found',
|
||||
'APs, clientes e ataques': 'APs, clients and attacks',
|
||||
'espectro e saúde': 'spectrum and health',
|
||||
'checar atualização': 'check for update',
|
||||
'checando…': 'checking…',
|
||||
'atualizar para {n}': 'update to {n}',
|
||||
@@ -15,7 +19,7 @@ export const EN = {
|
||||
'modo monitor': 'monitor mode',
|
||||
'modo gerenciado': 'managed mode',
|
||||
|
||||
// elevação / avisos
|
||||
// elevation / notices
|
||||
'Pedindo privilégio de administrador — necessário pra modo monitor e captura…':
|
||||
'Requesting administrator privilege — needed for monitor mode and capture…',
|
||||
'tentar de novo': 'try again',
|
||||
@@ -23,14 +27,19 @@ export const EN = {
|
||||
'Use somente em redes próprias ou com autorização explícita do responsável. Toda ativação de modo monitor e sessão de captura fica registrada na trilha de auditoria local.':
|
||||
'Use only on your own networks or with the explicit authorization of the responsible party. Every monitor mode activation and capture session is logged in the local audit trail.',
|
||||
|
||||
// controles
|
||||
// controls
|
||||
'ativar modo monitor': 'enable monitor mode',
|
||||
'desativar modo monitor': 'disable monitor mode',
|
||||
'iniciar captura': 'start capture',
|
||||
'parar captura': 'stop capture',
|
||||
|
||||
// resultados
|
||||
// results
|
||||
'Redes': 'Networks',
|
||||
'Redes vistas': 'Networks seen',
|
||||
'Linha do tempo': 'Timeline',
|
||||
'Linha do tempo ·': 'Timeline ·',
|
||||
'fechar': 'close',
|
||||
'motivo {n}': 'reason {n}',
|
||||
'Pronto pra capturar': 'Ready to capture',
|
||||
'Ative o modo monitor e inicie a captura pra ver as redes no ar.': 'Enable monitor mode and start capturing to see the networks on the air.',
|
||||
'Nenhuma interface Wi-Fi': 'No Wi-Fi interface',
|
||||
@@ -41,16 +50,44 @@ export const EN = {
|
||||
'oculta': 'hidden',
|
||||
'Rede': 'Network',
|
||||
'Canal': 'Channel',
|
||||
'Canais': 'Channels',
|
||||
'Banda': 'Band',
|
||||
'Bandas': 'Bands',
|
||||
'Nome': 'Name',
|
||||
'Sinal': 'Signal',
|
||||
'Segurança': 'Security',
|
||||
'Clientes': 'Clients',
|
||||
'filtrar por SSID ou BSSID': 'filter by SSID or BSSID',
|
||||
'Filtrar redes': 'Filter networks',
|
||||
'Ordenar por': 'Sort by',
|
||||
'expandir': 'expand',
|
||||
'recolher': 'collapse',
|
||||
'PMF parcial': 'partial PMF',
|
||||
'canal {n}': 'channel {n}',
|
||||
'canal {n} · {n}': 'channel {n} · {n}',
|
||||
'1 cliente': '1 client',
|
||||
'{n} clientes': '{n} clients',
|
||||
'{n} de {n} redes': '{n} of {n} networks',
|
||||
'{n} redes · {n} BSSIDs · {n} clientes': '{n} networks · {n} BSSIDs · {n} clients',
|
||||
'Nenhuma rede corresponde ao filtro': 'No network matches the filter',
|
||||
'Ajuste a busca ou limpe o campo de filtro.': 'Adjust the search or clear the filter.',
|
||||
|
||||
// erros de UI
|
||||
// UI errors
|
||||
'Não foi possível trocar o modo:': 'Could not change the mode:',
|
||||
'Não foi possível iniciar a captura:': 'Could not start the capture:',
|
||||
'Não foi possível retomar a captura: {n}': 'Could not resume the capture: {n}',
|
||||
'Retomando a captura e localizando a rede…': 'Resuming the capture and locating the network…',
|
||||
'A rede não reapareceu na varredura a tempo. Tente de novo.': 'The network did not reappear in the scan in time. Try again.',
|
||||
'A captura parou:': 'The capture stopped:',
|
||||
'erro desconhecido': 'unknown error',
|
||||
'rota desconhecida': 'unknown route',
|
||||
|
||||
// seletor de idioma
|
||||
// language selector / settings
|
||||
'Configurações': 'Settings',
|
||||
'Tema': 'Theme',
|
||||
'Claro': 'Light',
|
||||
'Escuro': 'Dark',
|
||||
'Fechar': 'Close',
|
||||
'Idioma': 'Language',
|
||||
'Auto': 'Auto',
|
||||
'Português': 'Portuguese',
|
||||
@@ -95,4 +132,436 @@ export const EN = {
|
||||
'nenhum handshake quebrável para os filtros informados': 'no crackable handshake for the given filters',
|
||||
'quantidade de quadros deve ficar entre 1 e 256': 'frame count must be between 1 and 256',
|
||||
'MAC inválido: {n}': 'invalid MAC: {n}',
|
||||
|
||||
// target details and attacks
|
||||
'não': 'no',
|
||||
'não anunciado': 'not advertised',
|
||||
'quebrável': 'crackable',
|
||||
'Nenhum handshake capturado para este BSSID.': 'No handshake captured for this BSSID.',
|
||||
'Rede aberta.': 'Open network.',
|
||||
'O tráfego não é cifrado pelo Wi-Fi; qualquer cliente pode se associar.': 'Traffic is not encrypted over Wi-Fi; any client can associate.',
|
||||
'A cifra é quebrável; capture IVs na aba Ataques.': 'The cipher is breakable; capture IVs in the Attacks tab.',
|
||||
'O AP anuncia configuração travada.': 'The AP advertises configuration locked.',
|
||||
'PIN pode estar acessível; verifique o lock.': 'The PIN may be accessible; check the lock.',
|
||||
'Desautenticação e captura de handshake podem ser ignoradas pelos clientes.': 'Deauthentication and handshake capture may be ignored by clients.',
|
||||
'Sem PMF obrigatório.': 'No mandatory PMF.',
|
||||
'Desautenticação tende a funcionar e facilita a captura do handshake.': 'Deauthentication tends to work and makes handshake capture easier.',
|
||||
'País': 'Country',
|
||||
'Último visto': 'Last seen',
|
||||
'Primeiro visto': 'First seen',
|
||||
'Frequência': 'Frequency',
|
||||
'Largura': 'Width',
|
||||
'Padrão': 'Standard',
|
||||
'Sinal máximo': 'Max signal',
|
||||
'Intervalo de beacon': 'Beacon interval',
|
||||
'Quadros de dados': 'Data frames',
|
||||
'Versão WPS': 'WPS version',
|
||||
'Vistos na rede': 'Seen on network',
|
||||
'Quebráveis': 'Crackable',
|
||||
'Número do modelo': 'Model number',
|
||||
'Nome do dispositivo': 'Device name',
|
||||
'Serial': 'Serial',
|
||||
'Identificação': 'Identity',
|
||||
'Rádio': 'Radio',
|
||||
'sim': 'yes',
|
||||
'Nome exibido': 'Display name',
|
||||
'Hostname': 'Hostname',
|
||||
'Sistema': 'OS',
|
||||
'IP': 'IP',
|
||||
'Associado a': 'Associated to',
|
||||
'Atual': 'Current',
|
||||
'Máximo': 'Maximum',
|
||||
'Sem amostras de sinal suficientes ainda.': 'Not enough signal samples yet.',
|
||||
'Linha do tempo · eventos mais recentes primeiro.': 'Timeline · most recent events first.',
|
||||
'Cliente · {n}': 'Client · {n}',
|
||||
'A captura parou sozinha:': 'The capture stopped on its own:',
|
||||
'A placa não está capturando nada': 'The adapter is not capturing anything',
|
||||
'Nenhum quadro recebido depois de alguns segundos saltando canais — esta placa pode aceitar ativar o modo monitor sem conseguir capturar de verdade (comum em Wi-Fi interna de notebook). Tente um adaptador USB dedicado a modo monitor/injeção.':
|
||||
'No frame received after several seconds of channel hopping — this adapter may accept turning monitor mode on without actually being able to capture (common with a laptop\'s built-in Wi-Fi). Try a USB adapter dedicated to monitor mode/injection.',
|
||||
'motivo desconhecido — confira se o adaptador ainda está conectado.':
|
||||
'unknown reason — check whether the adapter is still connected.',
|
||||
'Clientes e handshake': 'Clients and handshake',
|
||||
'Nenhum cliente visto.': 'No client seen.',
|
||||
'Associe ou gere tráfego na rede para que o Sabiá identifique estações.': 'Associate or generate traffic on the network so Sabiá can identify stations.',
|
||||
'senha encontrada': 'password found',
|
||||
'concluído': 'done',
|
||||
'quebrando': 'cracking',
|
||||
'senhas testadas': 'passwords tried',
|
||||
'{n} senhas testadas': '{n} passwords tried',
|
||||
'{n} tentativas · {n} ms': '{n} attempts · {n} ms',
|
||||
'Senha não encontrada no dicionário ({n} tentativas).': 'Password not found in the wordlist ({n} attempts).',
|
||||
'· quebrável': '· crackable',
|
||||
'não especificado': 'unspecified',
|
||||
'autenticação expirada': 'authentication expired',
|
||||
'saindo': 'leaving',
|
||||
'inativo': 'inactive',
|
||||
'AP sem recursos': 'AP out of resources',
|
||||
'classe 2': 'class 2',
|
||||
'classe 3': 'class 3',
|
||||
'{n} handshake(s) visto(s); {n} quebrável(is).': '{n} handshake(s) seen; {n} crackable.',
|
||||
'Nenhum handshake capturado ainda. Desautentique um cliente para estimular a reassociação.': 'No handshake captured yet. Deauthenticate a client to trigger reassociation.',
|
||||
'Captura WEP': 'WEP capture',
|
||||
'WEP não tem handshake: o ataque coleta IVs em um .pcap. Reinicie a captura gravando o arquivo e gere tráfego com desautenticação. A captura atual é reiniciada e a tabela começa vazia.': 'WEP has no handshake: the attack collects IVs into a .pcap. Restart the capture writing the file and generate traffic with deauthentication. The current capture is restarted and the table starts empty.',
|
||||
'Arquivo pcap': 'pcap file',
|
||||
'Reiniciar captura com pcap': 'Restart capture to pcap',
|
||||
'Gerar tráfego': 'Generate traffic',
|
||||
'Copiar comando': 'Copy command',
|
||||
'Esta rede não usa WEP.': 'This network does not use WEP.',
|
||||
'Somente em redes próprias ou com autorização escrita.': 'Only on your own networks or with written authorization.',
|
||||
'Cada ação fica registrada na trilha de auditoria local, com interface, BSSID e estação.': 'Every action is logged in the local audit trail with interface, BSSID and station.',
|
||||
'Tenho autorização para testar esta rede': 'I have authorization to test this network',
|
||||
'Desautenticação': 'Deauthentication',
|
||||
'Expulsa uma estação (ou todas) para forçar reassociação e captura de handshake.': 'Kicks a station (or all) to force reassociation and handshake capture.',
|
||||
'Enviar desautenticação': 'Send deauthentication',
|
||||
'Forçar handshake': 'Force handshake',
|
||||
'Dicionário': 'Wordlist',
|
||||
'Rede oculta': 'Hidden network',
|
||||
'job não encontrado': 'job not found',
|
||||
'{n} quadro(s) enviados para {n} (motivo {n}).': '{n} frame(s) sent to {n} (reason {n}).',
|
||||
'Selecione um handshake.': 'Select a handshake.',
|
||||
'Informe o dicionário.': 'Enter the wordlist.',
|
||||
'Informe o caminho do .pcap.': 'Enter the .pcap path.',
|
||||
'Força bruta por máscara — sem dicionário, testa toda combinação do padrão.':
|
||||
'Mask brute force — no wordlist, tests every combination of the pattern.',
|
||||
'dígito,': 'digit,',
|
||||
'minúscula,': 'lowercase,',
|
||||
'maiúscula,': 'uppercase,',
|
||||
'símbolo,': 'symbol,',
|
||||
'qualquer um; o resto é literal.': 'any of those; the rest is literal.',
|
||||
'Máscara': 'Mask',
|
||||
'Força bruta por máscara': 'Mask brute force',
|
||||
'Informe uma máscara (ex.: ?d?d?d?d?d?d?d?d).': 'Enter a mask (e.g. ?d?d?d?d?d?d?d?d).',
|
||||
'job não iniciado': 'job not started',
|
||||
'Captura gravando em {n}. Gere tráfego e aguarde IVs suficientes.': 'Capture writing to {n}. Generate traffic and wait for enough IVs.',
|
||||
'Visão geral': 'Overview',
|
||||
'Ver detalhes': 'View details',
|
||||
'Ver clientes': 'View clients',
|
||||
'Abrir ataques': 'Open attacks',
|
||||
'Todos (broadcast)': 'All (broadcast)',
|
||||
'associado': 'associated',
|
||||
'sondando': 'probing',
|
||||
'visto': 'seen',
|
||||
'Alvo': 'Target',
|
||||
'Handshake': 'Handshake',
|
||||
'Salvar pcap em': 'Save pcap to',
|
||||
'Exportar selecionado': 'Export selected',
|
||||
'Exportar todos': 'Export all',
|
||||
'Quebrar senha': 'Crack password',
|
||||
'completo': 'complete',
|
||||
'Ataques': 'Attacks',
|
||||
'Cliente alvo': 'Target client',
|
||||
'Ações': 'Actions',
|
||||
'Atacar': 'Attack',
|
||||
'Cliente não associado a este AP.': 'Client not associated with this AP.',
|
||||
'Somente o cliente selecionado será desautenticado.': 'Only the selected client will be deauthenticated.',
|
||||
'Todos os clientes associados (broadcast).': 'All associated clients (broadcast).',
|
||||
'Fabricante': 'Manufacturer',
|
||||
'Modelo': 'Model',
|
||||
'Oculta': 'Hidden',
|
||||
'Modo': 'Mode',
|
||||
'Cifra': 'Cipher',
|
||||
'Privacidade': 'Privacy',
|
||||
'WPS travado': 'WPS locked',
|
||||
'Associados': 'Associated',
|
||||
'Senha': 'Password',
|
||||
'Quadro': 'Frame',
|
||||
'Quadros': 'Frames',
|
||||
'ambas': 'both',
|
||||
'Tentativas': 'Attempts',
|
||||
'Testar PMF': 'Test PMF',
|
||||
'em execução': 'running',
|
||||
'iniciando': 'starting',
|
||||
'erro': 'error',
|
||||
'parado': 'stopped',
|
||||
'Status: {n}': 'Status: {n}',
|
||||
'tamanho inválido': 'invalid length',
|
||||
'· última: {n}': '· last: {n}',
|
||||
'Clona o AP (aberto) numa segunda interface e sobe um portal pedindo a senha de novo. A senha só é aceita se bater de verdade com o handshake já capturado — não é um "aceita qualquer coisa".':
|
||||
'Clones the AP (open) on a second interface and shows a portal asking for the password again. The password is only accepted if it truly matches the already-captured handshake — it is not an "accept anything" page.',
|
||||
'Exige um handshake quebrável já capturado deste AP (aba Handshake). Precisa de':
|
||||
'Requires a crackable handshake already captured for this AP (Handshake tab). Needs',
|
||||
'instalado e de uma': 'installed and a',
|
||||
'interface Wi-Fi em modo AP, diferente da que está capturando.':
|
||||
'Wi-Fi interface in AP mode, different from the one capturing.',
|
||||
'Iniciar Evil Twin': 'Start Evil Twin',
|
||||
'Informe a interface do clone (uma segunda placa).':
|
||||
'Enter the clone interface (a second adapter).',
|
||||
'a interface do clone precisa ser diferente da que está capturando':
|
||||
'the clone interface must be different from the one capturing',
|
||||
'esse BSSID ainda não apareceu nesta sessão de captura':
|
||||
'that BSSID has not appeared in this capture session yet',
|
||||
'rede oculta: sem SSID não dá pra clonar': 'hidden network: no SSID, cannot clone',
|
||||
'handshake sem dados suficientes pra conferir senha':
|
||||
'handshake does not have enough data to verify a password',
|
||||
'hostapd não está instalado (sudo apt install hostapd) — necessário pro Evil Twin':
|
||||
'hostapd is not installed (sudo apt install hostapd) — needed for Evil Twin',
|
||||
'{n} · {n} credencial(is) capturada(s){n}': '{n} · {n} credential(s) captured{n}',
|
||||
'Clona o AP numa segunda interface e deixa o hostapd-wpe aceitar a autenticação EAP de qualquer cliente. A credencial sai sozinha, como parte do próprio 802.1X — sem portal, sem ação humana.':
|
||||
'Clones the AP on a second interface and lets hostapd-wpe accept the EAP authentication from any client. The credential comes out on its own, as part of 802.1X itself — no portal, no human action.',
|
||||
'Rede Enterprise detectada (AKM 802.1X) — aqui a credencial é enviada automaticamente pelo cliente durante o handshake EAP, não precisa de portal. Exige':
|
||||
'Enterprise network detected (AKM 802.1X) — here the credential is sent automatically by the client during the EAP handshake, no portal needed. Requires',
|
||||
'instalado (não vem com o Sabiá: é um patch no build do hostapd —':
|
||||
'installed (not bundled with Sabiá: it is a patch on the hostapd build —',
|
||||
') e uma': ') and a',
|
||||
'interface Wi-Fi em modo AP.': 'Wi-Fi interface in AP mode.',
|
||||
'Iniciar Evil Twin Enterprise': 'Start Enterprise Evil Twin',
|
||||
'Limitação honesta: o certificado TLS do clone é autoassinado. Dispositivo que valida a cadeia de certificado da rede (comum em perfil corporativo/MDM atualizado) vai recusar ou avisar o usuário — essa é a defesa que deveria existir. Funciona contra configuração sem essa validação.':
|
||||
'Honest limitation: the clone\'s TLS certificate is self-signed. A device that validates the network\'s certificate chain (common on an updated corporate/MDM profile) will refuse or warn the user — that is the defense that is supposed to exist. It works against configurations without that validation.',
|
||||
'essa rede não é WPA2/WPA3-Enterprise (802.1X) — pra rede com senha única (PSK), use o Evil Twin com portal cativo':
|
||||
'this network is not WPA2/WPA3-Enterprise (802.1X) — for a single-password (PSK) network, use the Evil Twin with captive portal',
|
||||
'hostapd-wpe não está instalado — não vem com o Sabiá (é um patch sobre o build do hostapd, não um binário pequeno); instale pelo repositório do Kali ou compile https://github.com/aircrack-ng/hostapd-wpe':
|
||||
'hostapd-wpe is not installed — not bundled with Sabiá (it is a patch on the hostapd build, not a small binary); install it from the Kali repository or build https://github.com/aircrack-ng/hostapd-wpe',
|
||||
'{n} está num modo que o hostapd-wpe não consegue usar':
|
||||
'{n} is in a mode hostapd-wpe cannot use',
|
||||
'não foi possível gerar o certificado TLS (openssl) pro hostapd-wpe':
|
||||
'could not generate the TLS certificate (openssl) for hostapd-wpe',
|
||||
'hostapd-wpe saiu sozinho ({n}) logo ao subir em {n} — a placa pode não suportar de verdade o canal/banda do AP real. Saída: {n}':
|
||||
'hostapd-wpe exited on its own ({n}) right after starting on {n} — the adapter may not actually support the real AP\'s channel/band. Output: {n}',
|
||||
'job de Evil Twin Enterprise não encontrado': 'Enterprise Evil Twin job not found',
|
||||
'job de Evil Twin não encontrado': 'Evil Twin job not found',
|
||||
'hostapd saiu sozinho ({n}) logo ao subir em {n} — a placa pode não suportar de verdade o canal/banda do AP real. Saída: {n}':
|
||||
'hostapd exited on its own ({n}) right after starting on {n} — the adapter may not actually support the real AP\'s channel/band. Output: {n}',
|
||||
'{n} está num modo que o hostapd não consegue usar': '{n} is in a mode hostapd cannot use',
|
||||
'{n} handshake(s), {n} quadro(s) em {n}': '{n} handshake(s), {n} frame(s) in {n}',
|
||||
'Motivo': 'Reason',
|
||||
'Pacotes': 'Packets',
|
||||
'Estado': 'State',
|
||||
'Probes': 'Probes',
|
||||
'Cliente': 'Client',
|
||||
'WPS ativo': 'WPS active',
|
||||
'WPS ativo.': 'WPS active.',
|
||||
'WPS ativo ({n}).': 'WPS active ({n}).',
|
||||
'PMF exigido.': 'PMF required.',
|
||||
'Depois de coletar IVs suficientes:': 'After collecting enough IVs:',
|
||||
'mensagens {n}': 'messages {n}',
|
||||
'{n} · mensagens {n}': '{n} · messages {n}',
|
||||
'senha: {n}': 'password: {n}',
|
||||
'{n} · quebrável': '{n} · crackable',
|
||||
'{n} · canal {n}': '{n} · channel {n}',
|
||||
'{n} ({n})': '{n} ({n})',
|
||||
|
||||
// channels, PMKID/PMF, WPS, and report
|
||||
'Ocupação dos canais em {n}': 'Channel occupancy on {n}',
|
||||
'Sem dados de canal ainda': 'No channel data yet',
|
||||
'Inicie a captura para o Sabiá medir ocupação e sugerir canais.':
|
||||
'Start capturing so Sabiá can measure occupancy and suggest channels.',
|
||||
'{n} APs · {n} canais ocupados': '{n} APs · {n} busy channels',
|
||||
'{n} AP(s) · recomendado': '{n} AP(s) · recommended',
|
||||
'· recomendado': '· recommended',
|
||||
'aberta/WEP': 'open/WEP',
|
||||
'{n} rede(s) WPA3 analisada(s) — detalhes na aba do alvo e no relatório.':
|
||||
'{n} WPA3 network(s) analyzed — details in the target tab and report.',
|
||||
'Restrito a 1, 6 e 11 para evitar sobreposição.':
|
||||
'Limited to 1, 6 and 11 to avoid overlap.',
|
||||
'Menor pontuação de interferência entre os canais observados.':
|
||||
'Lowest interference score among observed channels.',
|
||||
'Melhor canal': 'Best channel',
|
||||
'Espectro e canais': 'Spectrum and channels',
|
||||
|
||||
// spectrum health (channels, SMART style)
|
||||
'Boa': 'Good',
|
||||
'Atenção': 'Attention',
|
||||
'Saturado': 'Saturated',
|
||||
'Boa condição': 'Good condition',
|
||||
'Sem canal livre nessa banda': 'No free channel on this band',
|
||||
'Até o melhor canal está saturado': 'Even the best channel is saturated',
|
||||
'Recomendado é DFS — pode cair com radar por perto': 'The recommended channel is DFS — it can drop off if radar is detected nearby',
|
||||
'Espectro congestionado': 'Congested spectrum',
|
||||
'Canal recomendado': 'Recommended channel',
|
||||
'Canais ocupados': 'Busy channels',
|
||||
'APs na banda': 'APs on this band',
|
||||
'Pior canal': 'Worst channel',
|
||||
'Canais DFS': 'DFS channels',
|
||||
'Nessa banda, prefira sempre 1, 6 ou 11 — canais no meio sobrepõem vizinhos.':
|
||||
'On this band, always prefer 1, 6 or 11 — channels in between overlap neighbors.',
|
||||
'Canais DFS liberam mais espaço, mas o rádio é obrigado a sair do ar se detectar radar — tenha um canal não-DFS de reserva.':
|
||||
'DFS channels free up more room, but the radio must go off the air if it detects radar — keep a non-DFS channel as backup.',
|
||||
'Considere mudar o AP para o canal recomendado ou mover pra outra banda.':
|
||||
'Consider moving the AP to the recommended channel or to another band.',
|
||||
'Canais detalhados ({n})': 'Detailed channels ({n})',
|
||||
'Pontuação': 'Score',
|
||||
'APs': 'APs',
|
||||
'{n} AP(s)': '{n} AP(s)',
|
||||
'Recomendado': 'Recommended',
|
||||
'Ainda sem eventos além do primeiro avistamento.':
|
||||
'No events beyond the first sighting yet.',
|
||||
'{n} amostra(s) de sinal. Eventos mais recentes primeiro.':
|
||||
'{n} signal sample(s). Most recent events first.',
|
||||
'nenhuma ferramenta instalada': 'no tool installed',
|
||||
'Senha encontrada: {n}{n}': 'Password found: {n}{n}',
|
||||
'Sem PIN, o reaver usa Pixie Dust; o bully usa o ataque padrão.':
|
||||
'Without a PIN, reaver uses Pixie Dust; bully uses its default attack.',
|
||||
'Instale reaver ou bully para habilitar. Ex.: sudo apt install reaver bully':
|
||||
'Install reaver or bully to enable. E.g.: sudo apt install reaver bully',
|
||||
'A saída do reaver/bully aparece aqui.': 'reaver/bully output appears here.',
|
||||
'Orquestra reaver/bully na interface monitor. O canal do AP é usado automaticamente; o processo pode ser interrompido a qualquer momento.':
|
||||
'Orchestrates reaver/bully on the monitor interface. The AP channel is used automatically; the process can be stopped at any time.',
|
||||
'Ferramenta': 'Tool',
|
||||
'PIN conhecido (opcional)': 'Known PIN (optional)',
|
||||
'Exportar .hc22000': 'Export .hc22000',
|
||||
'Todos .hc22000': 'Export all .hc22000',
|
||||
'Iniciar WPS': 'Start WPS',
|
||||
'Parar job': 'Stop job',
|
||||
'8 dígitos': '8 digits',
|
||||
'nenhum cliente visto': 'no client seen',
|
||||
'Direção': 'Direction',
|
||||
'AP → cliente': 'AP → client',
|
||||
'cliente → AP': 'client → AP',
|
||||
'Dispara deauth nas duas direções e observa se a estação reassocia. Resultado prático, não prova criptográfica.':
|
||||
'Sends deauth in both directions and watches whether the station reassociates. Practical result, not cryptographic proof.',
|
||||
'Estação': 'Station',
|
||||
'Forja um MAC local e envia association request com PMKID para tentar a mensagem 1. Muitos APs só expõem PMKID em roaming real.':
|
||||
'Forge a local MAC and send an association request with PMKID to try for message 1. Many APs only expose PMKID during real roaming.',
|
||||
'Caçar PMKID': 'Hunt PMKID',
|
||||
'Do cliente para o AP (exige estação selecionada).':
|
||||
'From client to AP (requires a selected station).',
|
||||
'Nas duas direções (exige estação selecionada).':
|
||||
'In both directions (requires a selected station).',
|
||||
'{n} quadro(s) {n} em {n} para {n} (motivo {n}).':
|
||||
'{n} {n} frame(s) in {n} to {n} (reason {n}).',
|
||||
'Aguardando saída do reaver/bully…': 'Waiting for reaver/bully output…',
|
||||
'Enviando associação com PMKID…': 'Sending association with PMKID…',
|
||||
'{n} quadro(s) enviados; PMKID não apareceu. {n}':
|
||||
'{n} frame(s) sent; PMKID did not appear. {n}',
|
||||
'Nenhuma estação disponível.': 'No station available.',
|
||||
'Testando por ~4 s…': 'Testing for ~4 s…',
|
||||
'PMKID capturado: {n}': 'PMKID captured: {n}',
|
||||
'PMF não obrigatório': 'PMF not required',
|
||||
'PMF não obrigatório · {n}': 'PMF not required · {n}',
|
||||
'Inconclusivo · {n}': 'Inconclusive · {n}',
|
||||
'Instale reaver ou bully.': 'Install reaver or bully.',
|
||||
'Canal do AP desconhecido.': 'Unknown AP channel.',
|
||||
'Montando o relatório…': 'Building the report…',
|
||||
'não foi possível gerar o relatório': 'could not generate the report',
|
||||
'Pré-visualização A4 · {n} página(s).': 'A4 preview · {n} page(s).',
|
||||
'Pré-visualização A4.': 'A4 preview.',
|
||||
'Paginando o relatório…': 'Paginating the report…',
|
||||
'Não foi possível abrir a impressão; baixe o HTML e abra no navegador.':
|
||||
'Could not open printing; download the HTML and open it in the browser.',
|
||||
'não foi possível gerar o HTML': 'could not generate the HTML',
|
||||
'Relatório': 'Report',
|
||||
'Relatório de auditoria': 'Audit report',
|
||||
'Imprimir / PDF': 'Print / PDF',
|
||||
'Baixar HTML': 'Download HTML',
|
||||
'Pré-visualização paginada em A4.': 'Paginated A4 preview.',
|
||||
'Visualização': 'View',
|
||||
'Relatório paginado': 'Paginated report',
|
||||
'handshake sem SSID, PMKID ou mensagem 2 para exportar':
|
||||
'handshake without SSID, PMKID or message 2 to export',
|
||||
'nenhum handshake com material para .hc22000':
|
||||
'no handshake with material for .hc22000',
|
||||
'direção inválida: use ap, sta ou both': 'invalid direction: use ap, sta or both',
|
||||
'máscara grande demais pra calcular o total': 'mask too large to calculate the total',
|
||||
'máscara tem {n} combinações, acima do limite de {n} — restrinja a máscara (ex.: menos posições ?a/?l) antes de rodar':
|
||||
'mask has {n} combinations, above the limit of {n} — narrow the mask (e.g. fewer ?a/?l positions) before running',
|
||||
'quadro inválido: use deauth ou disassoc': 'invalid frame: use deauth or disassoc',
|
||||
'direção sta/both exige a estação alvo': 'sta/both direction requires the target station',
|
||||
'inicie a captura nesta interface antes do teste PMKID':
|
||||
'start capturing on this interface before the PMKID test',
|
||||
'BSSID ainda não visto nesta captura': 'BSSID not seen in this capture yet',
|
||||
'SSID oculto: impossível montar o association request':
|
||||
'hidden SSID: cannot build the association request',
|
||||
'PMKID observado; exporte em .hc22000 e quebre com hashcat ou pelo Sabiá.':
|
||||
'PMKID observed; export to .hc22000 and crack with hashcat or Sabiá.',
|
||||
'Nenhum PMKID apareceu. O AP pode não usar cache de PMKID ou só responder a roaming real; tente também desautenticar um cliente associado.':
|
||||
'No PMKID appeared. The AP may not cache PMKID or may only answer real roaming; also try deauthenticating an associated client.',
|
||||
'PIN WPS deve ter 8 dígitos': 'WPS PIN must have 8 digits',
|
||||
'a ferramenta terminou sem informar PSK': 'the tool finished without reporting a PSK',
|
||||
'job WPS não encontrado': 'WPS job not found',
|
||||
'Auditoria Wi-Fi · {n}': 'Wi-Fi audit · {n}',
|
||||
'{n} está em modo managed; ative o modo monitor antes do teste PMKID':
|
||||
'{n} is in managed mode; enable monitor mode before the PMKID test',
|
||||
'{n} não encontrado no PATH; instale reaver ou bully':
|
||||
'{n} not found in PATH; install reaver or bully',
|
||||
'{n} está em modo managed; ative o modo monitor antes do ataque WPS':
|
||||
'{n} is in managed mode; enable monitor mode before the WPS attack',
|
||||
'não foi possível executar {n}: {n}': 'could not run {n}: {n}',
|
||||
'{n} terminou com código {n}': '{n} exited with code {n}',
|
||||
|
||||
// WIDS findings (analysis.rs)
|
||||
'SSID “{n}” com seguranças diferentes': 'SSID “{n}” with different security levels',
|
||||
'O mesmo nome aparece em BSSIDs com níveis de segurança distintos: {n}. Pode ser um evil twin ou um AP mal configurado.':
|
||||
'The same name appears on BSSIDs with different security levels: {n}. It may be an evil twin or a misconfigured AP.',
|
||||
'Rajada de desautenticação': 'Deauthentication burst',
|
||||
'{n} quadros de desautenticação/desassociação no último minuto. Pode ser ataque de desautenticação ou um AP instável.':
|
||||
'{n} deauthentication/disassociation frames in the last minute. It may be a deauth attack or an unstable AP.',
|
||||
'Muitos BSSIDs novos': 'Many new BSSIDs',
|
||||
'{n} BSSIDs apareceram no último minuto — possível beacon flood ou ambiente muito denso.':
|
||||
'{n} BSSIDs appeared in the last minute — possible beacon flood or a very dense environment.',
|
||||
'Rede aberta: {n}': 'Open network: {n}',
|
||||
'Sem cifra no Wi-Fi; o tráfego fica exposto a qualquer estação no alcance.':
|
||||
'No Wi-Fi encryption; traffic is exposed to any station in range.',
|
||||
'WEP em {n}': 'WEP on {n}',
|
||||
'WEP é quebrável em minutos; trate a rede como aberta.':
|
||||
'WEP can be cracked in minutes; treat the network as open.',
|
||||
'WPA1 (TKIP) em {n}': 'WPA1 (TKIP) on {n}',
|
||||
'WPA1/TKIP tem fraquezas conhecidas; migre para WPA2/CCMP ou WPA3.':
|
||||
'WPA1/TKIP has known weaknesses; migrate to WPA2/CCMP or WPA3.',
|
||||
'WPS ativo em {n}': 'WPS active on {n}',
|
||||
'O AP anuncia WPS com configuração travada.': 'The AP advertises WPS with configuration locked.',
|
||||
'WPS sem lock explícito; PIN pode ser testado com reaver/bully.':
|
||||
'WPS without an explicit lock; the PIN can be tested with reaver/bully.',
|
||||
'Sem PMF em {n}': 'No PMF on {n}',
|
||||
'Desautenticação e desassociação não são protegidas; clientes podem ser expulsos para facilitar a captura de handshake.':
|
||||
'Deauthentication and disassociation are unprotected; clients can be expelled to make handshake capture easier.',
|
||||
'PMF inconsistente em {n}': 'Inconsistent PMF on {n}',
|
||||
'O AP anuncia WPA3, mas não anuncia PMF; verifique a configuração.':
|
||||
'The AP advertises WPA3 but not PMF; check the configuration.',
|
||||
'WPA3 com WPS em {n}': 'WPA3 with WPS on {n}',
|
||||
'WPS não faz parte do WPA3 e abre uma via mais fraca de associação.':
|
||||
'WPS is not part of WPA3 and opens a weaker association path.',
|
||||
'primeiro visto': 'first seen',
|
||||
'associou': 'associated',
|
||||
'desautenticado': 'deauthenticated',
|
||||
|
||||
'A estação voltou a se associar.': 'The station reassociated.',
|
||||
'O AP pode não usar cache de PMKID.': 'The AP may not cache PMKID.',
|
||||
|
||||
'A estação voltou a se associar após a desautenticação: PMF não está obrigatório (ou o cliente não o usa).':
|
||||
'The station reassociated after deauthentication: PMF is not required (or the client does not use it).',
|
||||
'A estação continuou trocando quadros depois da desautenticação; o quadro de gerenciamento provavelmente foi aceito.':
|
||||
'The station kept exchanging frames after deauthentication; the management frame was probably accepted.',
|
||||
'A estação não voltou a associar na janela observada. PMF pode estar ativo, ou o cliente simplesmente ficou inativo.':
|
||||
'The station did not reassociate in the observed window. PMF may be active, or the client simply stayed idle.',
|
||||
'Inconclusivo': 'Inconclusive',
|
||||
|
||||
// continuous deauth and client identification
|
||||
'Mantém os quadros saindo em série até você parar (ou até sair o handshake). O canal da captura fica travado no AP durante o job.':
|
||||
'Keeps frames going until you stop (or a handshake appears). The capture channel stays locked to the AP during the job.',
|
||||
'Iniciar contínuo': 'Start continuous',
|
||||
'Parar': 'Stop',
|
||||
'Rajada ({n})': 'Burst ({n})',
|
||||
'Pronto. O job contínuo trava a captura no canal do AP e para quando você mandar.':
|
||||
'Ready. The continuous job locks capture to the AP channel and stops when you say so.',
|
||||
'Job {n} · {n} quadro(s){n}{n}': 'Job {n} · {n} frame(s){n}{n}',
|
||||
'Handshake capturado em {n} quadro(s){n}.': 'Handshake captured in {n} frame(s){n}.',
|
||||
'Job {n} {n} · {n} quadro(s){n}.': 'Job {n} {n} · {n} frame(s){n}.',
|
||||
'Escolha um cliente para desautenticar nas direções sta/both.':
|
||||
'Choose a client to deauthenticate in the sta/both directions.',
|
||||
'Forçando reassociação e handshake…': 'Forcing reassociation and handshake…',
|
||||
'Desautenticação contínua iniciada.': 'Continuous deauthentication started.',
|
||||
'Nenhuma ferramenta WPS disponível.': 'No WPS tool available.',
|
||||
'O reaver embutido usa Pixie Dust sem PIN. O bully aparece se estiver instalado no sistema.':
|
||||
'The bundled reaver uses Pixie Dust without a PIN. bully appears if it is installed on the system.',
|
||||
'O reaver embutido não pôde ser extraído nesta execução.':
|
||||
'The bundled reaver could not be extracted in this run.',
|
||||
'Dicionário (vazio = embutido, 9,7 milhões de senhas)': 'Wordlist (empty = bundled, 9.7 million passwords)',
|
||||
'dicionário próprio, opcional': 'custom wordlist, optional',
|
||||
'Aplicar regras de mutação (maiúscula, sufixos, leet — multiplica o tempo)':
|
||||
'Apply mutation rules (uppercase, suffixes, leetspeak — multiplies run time)',
|
||||
'Dispositivo': 'Device',
|
||||
'MAC aleatório': 'Random MAC',
|
||||
'Cliente {n}': 'Client {n}',
|
||||
'Linha do tempo · {n}': 'Timeline · {n}',
|
||||
'nenhuma ferramenta disponível': 'no tool available',
|
||||
'· canal {n}': '· channel {n}',
|
||||
'· para sozinho no handshake': '· stops on handshake',
|
||||
'job de deauth não encontrado': 'deauth job not found',
|
||||
'já existe um job de desautenticação em execução em {n}':
|
||||
'a deauthentication job is already running on {n}',
|
||||
'inicie a captura em {n} antes de iniciar o job':
|
||||
'start capture on {n} before starting the job',
|
||||
'a captura em {n} não está ativa': 'capture on {n} is not active',
|
||||
'{n} não está disponível; o reaver integrado não pôde ser extraído':
|
||||
'{n} is not available; the bundled reaver could not be extracted',
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
// Idioma da interface. O texto-fonte é em português; no modo inglês um tradutor
|
||||
// troca os textos exibidos (nós de texto e atributos) usando um dicionário, e
|
||||
// continua observando a página para traduzir o que for desenhado depois.
|
||||
// Textos com valores variáveis usam {n} no dicionário (ex.: "{n} redes" -> "{n} networks").
|
||||
// UI language. Source strings are in Portuguese; in English mode a translator
|
||||
// replaces displayed strings (text nodes and attributes) using a dictionary, and
|
||||
// keeps observing the page to translate dynamically rendered elements.
|
||||
// Variable placeholders use {n} in the dictionary (e.g. "{n} networks").
|
||||
|
||||
import { EN } from './i18n-en.js';
|
||||
|
||||
@@ -16,7 +16,7 @@ export function resolveLang(pref) {
|
||||
}
|
||||
|
||||
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
||||
// Dados do usuário (SSID, BSSID, MAC, valores mono) nunca são traduzidos.
|
||||
// User data (SSID, BSSID, MAC, monospace values) is never translated.
|
||||
const SKIP = '.ssid, .bssid, .mono, pre, code, input, textarea, script, style';
|
||||
|
||||
const exact = new Map(Object.entries(EN).filter(([k]) => !k.includes('{n}')));
|
||||
@@ -26,7 +26,7 @@ const patterns = Object.entries(EN)
|
||||
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: v };
|
||||
})
|
||||
// padrões mais específicos (mais texto fixo) primeiro
|
||||
// more specific patterns (more fixed text) first
|
||||
.sort((a, b) => b.re.source.length - a.re.source.length);
|
||||
|
||||
const cache = new Map();
|
||||
@@ -43,7 +43,7 @@ export function tr(text) {
|
||||
const m = core.match(p.re);
|
||||
if (m) {
|
||||
let i = 0;
|
||||
// os valores capturados também podem ter partes traduzíveis (ex.: estados)
|
||||
// captured values may also contain translatable parts (e.g. status labels)
|
||||
out = p.to.replace(/\{n\}/g, () => tr(m[++i] ?? ''));
|
||||
break;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export function tr(text) {
|
||||
return lead + out + trail;
|
||||
}
|
||||
|
||||
const orig = new WeakMap(); // nó -> texto original em português
|
||||
const orig = new WeakMap(); // node -> original source text
|
||||
let observer = null;
|
||||
|
||||
function translateNode(n) {
|
||||
|
||||
@@ -4,31 +4,53 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Sabiá</title>
|
||||
<link rel="icon" href="/sabia.png">
|
||||
<link rel="icon" href="/logo.png">
|
||||
<link rel="stylesheet" href="/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<img class="logo" src="/sabia.png" alt="">
|
||||
<img class="logo" src="/logo.png" alt="">
|
||||
<div>
|
||||
<strong>Sabiá</strong>
|
||||
<small>auditoria de Wi-Fi</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-title">Interfaces</div>
|
||||
<ul class="list" id="ifaceList" data-empty="nenhuma interface Wi-Fi encontrada"></ul>
|
||||
<div class="side-title">Interface</div>
|
||||
<div class="iface-pick">
|
||||
<select id="ifaceSelect" class="select" aria-label="Interface Wi-Fi" data-empty="nenhuma interface Wi-Fi encontrada"></select>
|
||||
<span id="modePill" class="pill"><span class="dot"></span><span id="modeLabel">—</span></span>
|
||||
</div>
|
||||
|
||||
<div class="side-title">Visualização</div>
|
||||
<ul class="list" id="viewNav" role="group" aria-label="Visualização">
|
||||
<li><button type="button" class="dev" data-view="nets" aria-pressed="true">
|
||||
<span class="ico"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1"/></svg></span>
|
||||
<span><span class="t">Redes</span><span class="s">APs, clientes e ataques</span></span>
|
||||
</button></li>
|
||||
<li><button type="button" class="dev" data-view="channels" aria-pressed="false">
|
||||
<span class="ico"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="19" x2="5" y2="10"/><line x1="12" y1="19" x2="12" y2="5"/><line x1="19" y1="19" x2="19" y2="13"/></svg></span>
|
||||
<span><span class="t">Canais</span><span class="s">espectro e saúde</span></span>
|
||||
</button></li>
|
||||
</ul>
|
||||
|
||||
<div class="side-foot">
|
||||
<span id="verInfo" class="mono">—</span>
|
||||
<select id="langSel" class="lang-sel" aria-label="Idioma" title="Idioma">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="pt">Português</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
<button id="btnUpdate" type="button">checar atualização</button>
|
||||
<div class="foot-row">
|
||||
<button id="settingsBtn" class="icon-btn" type="button" title="Configurações" aria-label="Configurações">
|
||||
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>
|
||||
</button>
|
||||
<div class="theme-switch" role="group" aria-label="Tema">
|
||||
<button type="button" data-theme-pick="auto" aria-pressed="true">Auto</button>
|
||||
<button type="button" data-theme-pick="light" aria-pressed="false">Claro</button>
|
||||
<button type="button" data-theme-pick="dark" aria-pressed="false">Escuro</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="foot-row">
|
||||
<span id="verInfo" class="mono">—</span>
|
||||
<button id="btnUpdate" type="button">checar atualização</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -39,7 +61,9 @@
|
||||
<h1>Painel de captura</h1>
|
||||
<div class="sub" id="ifaceSub">selecione uma interface</div>
|
||||
</div>
|
||||
<span id="modePill" class="pill"><span class="dot"></span><span id="modeLabel">—</span></span>
|
||||
<div class="top-actions">
|
||||
<button id="btnReport" class="btn" type="button" disabled>Relatório</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svg class="wave" id="wave" viewBox="0 0 600 34" preserveAspectRatio="none">
|
||||
@@ -68,14 +92,79 @@
|
||||
</div>
|
||||
|
||||
<div class="result-head">
|
||||
<strong>Redes vistas</strong>
|
||||
<span id="count">0</span>
|
||||
<div class="result-summary">
|
||||
<strong id="resultTitle">Redes vistas</strong>
|
||||
<span id="count">0</span>
|
||||
</div>
|
||||
<div class="result-tools">
|
||||
<input id="searchInput" class="filter" type="search" placeholder="filtrar por SSID ou BSSID" aria-label="Filtrar redes" autocomplete="off" spellcheck="false">
|
||||
<select id="sortSel" class="select compact" aria-label="Ordenar por">
|
||||
<option value="signal">Sinal</option>
|
||||
<option value="security">Segurança</option>
|
||||
<option value="clients">Clientes</option>
|
||||
<option value="name">Nome</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="resultArea"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<dialog id="targetDlg" class="target-dlg" aria-labelledby="targetTitle">
|
||||
<div class="target-head">
|
||||
<div class="target-id">
|
||||
<h2 id="targetTitle">—</h2>
|
||||
<div class="target-sub" id="targetSub"></div>
|
||||
</div>
|
||||
<button id="targetClose" class="icon-btn" type="button" aria-label="Fechar" title="Fechar">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tabs" role="tablist" aria-label="Detalhes do alvo">
|
||||
<button type="button" role="tab" data-tab="overview" aria-selected="true">Visão geral</button>
|
||||
<button type="button" role="tab" data-tab="clients" aria-selected="false">Clientes <span class="tab-count" id="tabClientsCount">0</span></button>
|
||||
<button type="button" role="tab" data-tab="attacks" aria-selected="false">Ataques</button>
|
||||
</div>
|
||||
<div class="tab-panel" id="tabPanel"></div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="reportDlg" class="report-dlg" aria-labelledby="reportTitle">
|
||||
<div class="report-head">
|
||||
<div>
|
||||
<h2 id="reportTitle">Relatório de auditoria</h2>
|
||||
<p class="hint" id="reportStatus">Pré-visualização paginada em A4.</p>
|
||||
</div>
|
||||
<div class="report-tools">
|
||||
<button type="button" class="icon-btn" id="reportZoomOut" title="Diminuir zoom" aria-label="Diminuir zoom">−</button>
|
||||
<span class="mono" id="reportZoomLabel">100%</span>
|
||||
<button type="button" class="icon-btn" id="reportZoomIn" title="Aumentar zoom" aria-label="Aumentar zoom">+</button>
|
||||
<button type="button" class="btn" id="reportPrint">Imprimir / PDF</button>
|
||||
<button type="button" class="btn" id="reportDownload">Baixar HTML</button>
|
||||
<button type="button" class="icon-btn" id="reportClose" title="Fechar" aria-label="Fechar">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-view" id="reportView"><div class="report-canvas" id="reportCanvas"><iframe id="reportFrame" title="Relatório paginado"></iframe></div></div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="settingsDlg" aria-labelledby="settingsTitle">
|
||||
<form method="dialog">
|
||||
<h2 id="settingsTitle">Configurações</h2>
|
||||
<div class="field">
|
||||
<span class="field-label">Idioma</span>
|
||||
<select id="langSel" class="select" name="lang" aria-label="Idioma">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="pt">Português</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<menu><button class="btn primary" value="close">Fechar</button></menu>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* @license Paged.js v0.4.3 | MIT | https://gitlab.coko.foundation/pagedjs/pagedjs
|
||||
*/
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).PagedPolyfill=t()}(this,(function(){"use strict";function e(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var t,n,r={exports:{}},a={exports:{}};var i,o,s,l,d=function(){},p=d(),c=function(e){return e!==p&&null!==e};function u(){return l||(l=1,s=(n?t:(n=1,t=function(){try{return Object.keys("primitive"),!0}catch(e){return!1}}))()?Object.keys:function(){if(o)return i;o=1;var e=c,t=Object.keys;return i=function(n){return t(e(n)?Object(n):n)}}()),s}var m,h,g=c,f=function(e){if(!g(e))throw new TypeError("Cannot use null or undefined");return e};var y,b,S=function(){var e,t=Object.assign;return"function"==typeof t&&(t(e={foo:"raz"},{bar:"dwa"},{trzy:"trzy"}),e.foo+e.bar+e.trzy==="razdwatrzy")}()?Object.assign:function(){if(h)return m;h=1;var e=u(),t=f,n=Math.max;return m=function(r,a){var i,o,s,l=n(arguments.length,2);for(r=Object(t(r)),s=function(e){try{r[e]=a[e]}catch(e){i||(i=e)}},o=1;o<l;++o)e(a=arguments[o]).forEach(s);if(void 0!==i)throw i;return r},m}(),v=c,x=Array.prototype.forEach,k=Object.create,w="razdwatrzy";var C,T="function"==typeof w.contains&&!0===w.contains("dwa")&&!1===w.contains("foo")?String.prototype.contains:function(){if(b)return y;b=1;var e=String.prototype.indexOf;return y=function(t){return e.call(this,t,arguments[1])>-1},y}(),_=S,z=function(e){var t=k(null);return x.call(arguments,(function(e){v(e)&&function(e,t){var n;for(n in e)t[n]=e[n]}(Object(e),t)})),t},O=function(e){return"function"==typeof e},A=T;C=a.exports=function(e,t){var n,r,a,i,o;return arguments.length<2||"string"!=typeof e?(i=t,t=e,e=null):i=arguments[2],null==e?(n=a=!0,r=!1):(n=A.call(e,"c"),r=A.call(e,"e"),a=A.call(e,"w")),o={value:t,configurable:n,enumerable:r,writable:a},i?_(z(i),o):o},C.gs=function(e,t,n){var r,a,i,o;return"string"!=typeof e?(i=n,n=t,t=e,e=null):i=arguments[3],null==t?t=void 0:O(t)?null==n?n=void 0:O(n)||(i=n,n=void 0):(i=t,t=n=void 0),null==e?(r=!0,a=!1):(r=A.call(e,"c"),a=A.call(e,"e")),o={get:t,set:n,configurable:r,enumerable:a},i?_(z(i),o):o};var E=a.exports,j=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e};!function(e,t){var n,r,a,i,o,s,l,d=E,p=j,c=Function.prototype.apply,u=Function.prototype.call,m=Object.create,h=Object.defineProperty,g=Object.defineProperties,f=Object.prototype.hasOwnProperty,y={configurable:!0,enumerable:!1,writable:!0};n=function(e,t){var n;return p(t),f.call(this,"__ee__")?n=this.__ee__:(n=y.value=m(null),h(this,"__ee__",y),y.value=null),n[e]?"object"==typeof n[e]?n[e].push(t):n[e]=[n[e],t]:n[e]=t,this},r=function(e,t){var r,i;return p(t),i=this,n.call(this,e,r=function(){a.call(i,e,r),c.call(t,this,arguments)}),r.__eeOnceListener__=t,this},a=function(e,t){var n,r,a,i;if(p(t),!f.call(this,"__ee__"))return this;if(!(n=this.__ee__)[e])return this;if("object"==typeof(r=n[e]))for(i=0;a=r[i];++i)a!==t&&a.__eeOnceListener__!==t||(2===r.length?n[e]=r[i?0:1]:r.splice(i,1));else r!==t&&r.__eeOnceListener__!==t||delete n[e];return this},i=function(e){var t,n,r,a,i;if(f.call(this,"__ee__")&&(a=this.__ee__[e]))if("object"==typeof a){for(n=arguments.length,i=new Array(n-1),t=1;t<n;++t)i[t-1]=arguments[t];for(a=a.slice(),t=0;r=a[t];++t)c.call(r,this,i)}else switch(arguments.length){case 1:u.call(a,this);break;case 2:u.call(a,this,arguments[1]);break;case 3:u.call(a,this,arguments[1],arguments[2]);break;default:for(n=arguments.length,i=new Array(n-1),t=1;t<n;++t)i[t-1]=arguments[t];c.call(a,this,i)}},o={on:n,once:r,off:a,emit:i},s={on:d(n),once:d(r),off:d(a),emit:d(i)},l=g({},s),e.exports=t=function(e){return null==e?m(l):g(Object(e),s)},t.methods=o}(r,r.exports);var W=r.exports,L=e(W);class B{constructor(e){this.context=e||this,this.hooks=[]}register(){for(var e=0;e<arguments.length;++e)if("function"==typeof arguments[e])this.hooks.push(arguments[e]);else for(var t=0;t<arguments[e].length;++t)this.hooks.push(arguments[e][t])}trigger(){var e=arguments,t=this.context,n=[];return this.hooks.forEach((function(r){var a=r.apply(t,e);a&&"function"==typeof a.then?n.push(a):n.push(new Promise(((e,t)=>{e(a)})))})),Promise.all(n)}triggerSync(){var e=arguments,t=this.context,n=[];return this.hooks.forEach((function(r){var a=r.apply(t,e);n.push(a)})),n}list(){return this.hooks}clear(){return this.hooks=[]}}function P(e){if(!e)return;let t;if(void 0!==e.getBoundingClientRect)t=e.getBoundingClientRect();else{let n=document.createRange();n.selectNode(e),t=n.getBoundingClientRect()}return t}function q(e){if(!e)return;let t;if(void 0!==e.getClientRects)t=e.getClientRects();else{let n=document.createRange();n.selectNode(e),t=n.getClientRects()}return t}function M(){var e=(new Date).getTime();return"undefined"!=typeof performance&&"function"==typeof performance.now&&(e+=performance.now()),"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replaceLine truncated
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sabia-cli"
|
||||
description = "sabia: CLI de auditoria e monitoramento de redes Wi-Fi"
|
||||
description = "Sabiá: Wi-Fi network auditing and monitoring CLI"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::time::Duration;
|
||||
version
|
||||
)]
|
||||
struct Cli {
|
||||
/// Trilha de auditoria (padrão: .sabia-audit.log).
|
||||
/// Audit log file path (default: `.sabia-audit.log`).
|
||||
#[arg(long, global = true, default_value = ".sabia-audit.log")]
|
||||
audit: PathBuf,
|
||||
|
||||
@@ -26,91 +26,95 @@ struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Cmd {
|
||||
/// Lista interfaces, canais e controla o modo monitor.
|
||||
/// List interfaces, channels, and toggle monitor mode.
|
||||
Iface {
|
||||
#[command(subcommand)]
|
||||
action: IfaceAction,
|
||||
},
|
||||
/// Captura o ar e mostra redes, clientes e handshakes.
|
||||
/// Capture live traffic and display networks, clients, and handshakes.
|
||||
Scan {
|
||||
iface: String,
|
||||
/// Canal fixo; sem isso, percorre os canais suportados.
|
||||
/// Fixed channel; when omitted, hops across supported channels.
|
||||
#[arg(long)]
|
||||
channel: Option<u32>,
|
||||
/// Banda ao percorrer canais: 2.4, 5, 6 ou all.
|
||||
/// Frequency band when hopping channels: `2.4`, `5`, `6`, or `all`.
|
||||
#[arg(long)]
|
||||
band: Option<String>,
|
||||
/// Tempo em cada canal (ms).
|
||||
/// Channel dwell time in milliseconds.
|
||||
#[arg(long, default_value_t = 350)]
|
||||
hop_ms: u64,
|
||||
/// Para a varredura depois de N segundos (padrão: até Ctrl+C).
|
||||
/// Stop scanning after N seconds (default: run until Ctrl+C).
|
||||
#[arg(long)]
|
||||
seconds: Option<u64>,
|
||||
/// Salva os quadros capturados em um .pcap.
|
||||
/// Save captured frames to a `.pcap` file.
|
||||
#[arg(long)]
|
||||
out: Option<PathBuf>,
|
||||
/// Depois de capturar, tenta quebrar o primeiro handshake.
|
||||
/// After capturing, attempt a wordlist crack on the first handshake.
|
||||
#[arg(long, value_name = "DICIONARIO")]
|
||||
crack: Option<PathBuf>,
|
||||
/// Mostra apenas o JSON final.
|
||||
/// Output only the final JSON summary.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Analisa um .pcap offline (redes, clientes e handshakes).
|
||||
/// Analyze a `.pcap` file offline (networks, clients, and handshakes).
|
||||
Analyze {
|
||||
pcap: PathBuf,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Quebra um handshake de um .pcap por dicionário.
|
||||
/// Run a dictionary attack against a handshake in a `.pcap` file.
|
||||
Crack {
|
||||
pcap: PathBuf,
|
||||
/// Dicionário com uma senha por linha.
|
||||
/// Wordlist file (one candidate passphrase per line).
|
||||
#[arg(short, long)]
|
||||
wordlist: PathBuf,
|
||||
#[arg(long)]
|
||||
bssid: Option<String>,
|
||||
#[arg(long)]
|
||||
station: Option<String>,
|
||||
/// Apply common mutation rules to each word (capitalization, numeric
|
||||
/// suffixes, simple leetspeak) — multiplies total runtime.
|
||||
#[arg(long)]
|
||||
rules: bool,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Envia desautenticação para um AP/estação (teste autorizado).
|
||||
/// Send deauthentication frames to an AP/station (authorized testing only).
|
||||
Deauth {
|
||||
iface: String,
|
||||
#[arg(long)]
|
||||
bssid: String,
|
||||
/// Sem isso, o destino é broadcast.
|
||||
/// Target client MAC; defaults to broadcast when omitted.
|
||||
#[arg(long)]
|
||||
station: Option<String>,
|
||||
#[arg(long, default_value_t = 5)]
|
||||
count: u32,
|
||||
#[arg(long, default_value_t = 7)]
|
||||
reason: u16,
|
||||
/// Confirma que você tem autorização para testar esta rede.
|
||||
/// Confirm explicit authorization to test this target network.
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
/// Diagnóstico de privilégios, ferramentas e interfaces.
|
||||
/// Diagnose privileges, external tools, and wireless interfaces.
|
||||
Doctor {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Inspeciona a trilha de auditoria.
|
||||
/// Inspect and verify the hash-chained audit trail.
|
||||
Audit {
|
||||
#[command(subcommand)]
|
||||
action: AuditAction,
|
||||
},
|
||||
/// Verifica e aplica atualizações do próprio sabia.
|
||||
/// Check for and apply `sabia` CLI updates.
|
||||
Update {
|
||||
#[arg(long)]
|
||||
apply: bool,
|
||||
},
|
||||
/// Abre a interface gráfica (servidor local + navegador).
|
||||
/// Launch the web UI (local HTTP server + browser).
|
||||
Ui {
|
||||
#[arg(long, default_value = "4337")]
|
||||
port: u16,
|
||||
/// Não tenta abrir o navegador automaticamente.
|
||||
/// Do not open the browser automatically.
|
||||
#[arg(long)]
|
||||
no_open: bool,
|
||||
},
|
||||
@@ -119,13 +123,13 @@ enum Cmd {
|
||||
#[derive(Subcommand)]
|
||||
enum IfaceAction {
|
||||
List,
|
||||
/// Mostra os canais suportados pelo rádio.
|
||||
/// List channels supported by the wireless phy.
|
||||
Channels {
|
||||
iface: String,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Liga/desliga modo monitor numa interface Wi-Fi.
|
||||
/// Enable or disable monitor mode on a Wi-Fi interface.
|
||||
Monitor {
|
||||
iface: String,
|
||||
#[arg(value_enum)]
|
||||
@@ -135,9 +139,9 @@ enum IfaceAction {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum AuditAction {
|
||||
/// Confere a integridade da cadeia.
|
||||
/// Verify the cryptographic hash chain integrity.
|
||||
Verify { path: Option<PathBuf> },
|
||||
/// Mostra as últimas entradas.
|
||||
/// Show the last N entries.
|
||||
Show {
|
||||
#[arg(long, default_value_t = 20)]
|
||||
limit: usize,
|
||||
@@ -275,6 +279,7 @@ fn run(cli: Cli) -> sabia_core::Result<()> {
|
||||
wordlist,
|
||||
bssid,
|
||||
station,
|
||||
rules,
|
||||
json,
|
||||
} => {
|
||||
let mut last = 0u64;
|
||||
@@ -283,6 +288,7 @@ fn run(cli: Cli) -> sabia_core::Result<()> {
|
||||
&wordlist,
|
||||
bssid.as_deref(),
|
||||
station.as_deref(),
|
||||
rules,
|
||||
|tried| {
|
||||
if tried.saturating_sub(last) >= 4096 {
|
||||
eprint!("\r{tried} senhas testadas…");
|
||||
@@ -412,7 +418,7 @@ fn crack_first(engine: &Engine, iface: &str, wordlist: &PathBuf) -> sabia_core::
|
||||
hs.bssid,
|
||||
wordlist.display()
|
||||
);
|
||||
let id = engine.start_crack(iface, Some(&hs.bssid), Some(&hs.station), wordlist)?;
|
||||
let id = engine.start_crack(iface, Some(&hs.bssid), Some(&hs.station), wordlist, false)?;
|
||||
loop {
|
||||
let job = engine.crack_job(&id).expect("job");
|
||||
if job.status == "running" {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Servidor local da interface: uma única página HTML/JS servida em
|
||||
//! `127.0.0.1`, protegida por um token gerado na hora, falando com o
|
||||
//! núcleo por uma API JSON simples. A mesma página em `apps/gui/ui` é o
|
||||
//! que o app desktop (Tauri) carrega — só muda o transporte.
|
||||
//! Local web UI server: serves the single-page HTML/JS app on
|
||||
//! `127.0.0.1`, protected by a per-session random token, communicating with
|
||||
//! the core over JSON endpoints. The exact same frontend in `apps/gui/ui`
|
||||
//! powers the Tauri desktop app.
|
||||
|
||||
use sabia_core::engine::{Engine, ScanOptions};
|
||||
use serde::Deserialize;
|
||||
@@ -15,13 +15,29 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
const INDEX_HTML: &str = include_str!("../../../../apps/gui/ui/index.html");
|
||||
const APP_CSS: &str = include_str!("../../../../apps/gui/ui/app.css");
|
||||
const APP_JS: &str = include_str!("../../../../apps/gui/ui/app.js");
|
||||
const LOGO_PNG: &[u8] = include_bytes!("../../../../apps/gui/ui/sabia.png");
|
||||
const I18N_JS: &str = include_str!("../../../../apps/gui/ui/i18n.js");
|
||||
const I18N_EN_JS: &str = include_str!("../../../../apps/gui/ui/i18n-en.js");
|
||||
const LOGO_PNG: &[u8] = include_bytes!("../../../../logo.png");
|
||||
const PAGED_JS: &str = include_str!("../../../../apps/gui/ui/vendor/paged.polyfill.min.js");
|
||||
|
||||
pub fn serve(port: u16, audit_path: std::path::PathBuf, open: bool) -> sabia_core::Result<()> {
|
||||
let token = gen_token();
|
||||
let listener = TcpListener::bind(("127.0.0.1", port))?;
|
||||
let engine = Arc::new(Engine::with_audit_path(audit_path));
|
||||
|
||||
// Without a signal handler, Ctrl+C (or `SIGTERM`) during an active Evil Twin
|
||||
// exits before cleanup runs: `hostapd`/`dnsmasq` are left orphaned
|
||||
// and both interfaces remain stuck in `managed no` under
|
||||
// NetworkManager until the next engine startup runs self-healing.
|
||||
//
|
||||
{
|
||||
let engine = engine.clone();
|
||||
let _ = ctrlc::set_handler(move || {
|
||||
engine.shutdown_cleanup();
|
||||
std::process::exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
println!("Sabiá UI em http://127.0.0.1:{port}/?token={token}");
|
||||
if open {
|
||||
let _ = open_browser(&format!("http://127.0.0.1:{port}/?token={token}"));
|
||||
@@ -225,7 +241,48 @@ fn handle(
|
||||
);
|
||||
}
|
||||
|
||||
if req.path == "/api/report.html" {
|
||||
if req.token.as_deref() != Some(expected_token) {
|
||||
return respond(&mut stream, 401, "text/plain", b"token invalido");
|
||||
}
|
||||
let Some(iface) = req.text("iface") else {
|
||||
return respond(&mut stream, 400, "text/plain", b"iface obrigatoria");
|
||||
};
|
||||
return match engine.report(&iface) {
|
||||
Ok(model) => {
|
||||
let download = req.bool("download");
|
||||
let mut html = sabia_core::report::render(
|
||||
&model,
|
||||
sabia_core::report::ReportOptions {
|
||||
inline_pagedjs: download,
|
||||
},
|
||||
);
|
||||
if download {
|
||||
html = html.replace(
|
||||
"<!--SABIA_PAGEDJS-->",
|
||||
&format!("<script>{PAGED_JS}</script>"),
|
||||
);
|
||||
}
|
||||
respond(
|
||||
&mut stream,
|
||||
200,
|
||||
"text/html; charset=utf-8",
|
||||
html.as_bytes(),
|
||||
)
|
||||
}
|
||||
Err(e) => respond(&mut stream, 400, "text/plain", e.to_string().as_bytes()),
|
||||
};
|
||||
}
|
||||
|
||||
match req.path.as_str() {
|
||||
"/vendor/paged.polyfill.min.js" | "/paged.polyfill.min.js" => {
|
||||
return respond(
|
||||
&mut stream,
|
||||
200,
|
||||
"application/javascript",
|
||||
PAGED_JS.as_bytes(),
|
||||
)
|
||||
}
|
||||
"/app.css" => return respond(&mut stream, 200, "text/css", APP_CSS.as_bytes()),
|
||||
"/app.js" => {
|
||||
return respond(
|
||||
@@ -235,7 +292,23 @@ fn handle(
|
||||
APP_JS.as_bytes(),
|
||||
)
|
||||
}
|
||||
"/sabia.png" => return respond(&mut stream, 200, "image/png", LOGO_PNG),
|
||||
"/i18n.js" => {
|
||||
return respond(
|
||||
&mut stream,
|
||||
200,
|
||||
"application/javascript",
|
||||
I18N_JS.as_bytes(),
|
||||
)
|
||||
}
|
||||
"/i18n-en.js" => {
|
||||
return respond(
|
||||
&mut stream,
|
||||
200,
|
||||
"application/javascript",
|
||||
I18N_EN_JS.as_bytes(),
|
||||
)
|
||||
}
|
||||
"/logo.png" => return respond(&mut stream, 200, "image/png", LOGO_PNG),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -408,13 +481,210 @@ fn api(engine: &Arc<Engine>, req: &Request) -> (u16, String) {
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/handshakes/export-hc22000") => {
|
||||
let (Some(iface), Some(bssid), Some(station), Some(out)) = (
|
||||
req.text("iface"),
|
||||
req.text("bssid"),
|
||||
req.text("station"),
|
||||
req.text("out"),
|
||||
) else {
|
||||
return err(400, "iface, bssid, station e out são obrigatórios");
|
||||
};
|
||||
match engine.export_hc22000(&iface, &bssid, &station, &out) {
|
||||
Ok(report) => ok(&report),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/handshakes/export-all-hc22000") => {
|
||||
let (Some(iface), Some(out)) = (req.text("iface"), req.text("out")) else {
|
||||
return err(400, "iface e out são obrigatórios");
|
||||
};
|
||||
match engine.export_all_hc22000(&iface, &out) {
|
||||
Ok(report) => ok(&report),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("GET", "/api/report") => {
|
||||
let Some(iface) = req.text("iface") else {
|
||||
return err(400, "iface obrigatória");
|
||||
};
|
||||
match engine.report(&iface) {
|
||||
Ok(model) => ok(&model),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("GET", "/api/wps/tools") => ok(&engine.wps_tools()),
|
||||
("POST", "/api/wps") => {
|
||||
if !req.bool("confirm") {
|
||||
return err(400, "confirme a autorização do teste (confirm=true)");
|
||||
}
|
||||
let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else {
|
||||
return err(400, "iface e bssid são obrigatórios");
|
||||
};
|
||||
let tool = req.text("tool").unwrap_or_else(|| "reaver".into());
|
||||
let channel = req.u32("channel").unwrap_or(0);
|
||||
let pin = req.text("pin");
|
||||
match engine.start_wps(&iface, &bssid, channel, &tool, pin.as_deref()) {
|
||||
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("GET", "/api/wps") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.wps_job(&id) {
|
||||
Some(job) => ok(&job),
|
||||
None => err(404, "job não encontrado"),
|
||||
}
|
||||
}
|
||||
("GET", "/api/wps/jobs") => ok(&engine.wps_jobs()),
|
||||
("POST", "/api/wps/stop") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.stop_wps(&id) {
|
||||
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/eviltwin") => {
|
||||
if !req.bool("confirm") {
|
||||
return err(400, "confirme a autorização do teste (confirm=true)");
|
||||
}
|
||||
let (Some(iface), Some(ap_iface), Some(bssid)) =
|
||||
(req.text("iface"), req.text("ap_iface"), req.text("bssid"))
|
||||
else {
|
||||
return err(400, "iface, ap_iface e bssid são obrigatórios");
|
||||
};
|
||||
let portal_port = req.u16("portal_port").unwrap_or(8080);
|
||||
let deauth = req.bool("deauth");
|
||||
match engine.start_evil_twin(&iface, &ap_iface, &bssid, portal_port, deauth) {
|
||||
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("GET", "/api/eviltwin") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.eviltwin_job(&id) {
|
||||
Some(job) => ok(&job),
|
||||
None => err(404, "job não encontrado"),
|
||||
}
|
||||
}
|
||||
("GET", "/api/eviltwins") => ok(&engine.eviltwin_jobs()),
|
||||
("POST", "/api/eviltwin/stop") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.stop_evil_twin(&id) {
|
||||
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/eviltwin-enterprise") => {
|
||||
if !req.bool("confirm") {
|
||||
return err(400, "confirme a autorização do teste (confirm=true)");
|
||||
}
|
||||
let (Some(iface), Some(ap_iface), Some(bssid)) =
|
||||
(req.text("iface"), req.text("ap_iface"), req.text("bssid"))
|
||||
else {
|
||||
return err(400, "iface, ap_iface e bssid são obrigatórios");
|
||||
};
|
||||
match engine.start_evil_twin_enterprise(&iface, &ap_iface, &bssid) {
|
||||
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("GET", "/api/eviltwin-enterprise") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.enterprise_twin_job(&id) {
|
||||
Some(job) => ok(&job),
|
||||
None => err(404, "job não encontrado"),
|
||||
}
|
||||
}
|
||||
("GET", "/api/eviltwins-enterprise") => ok(&engine.enterprise_twin_jobs()),
|
||||
("POST", "/api/eviltwin-enterprise/stop") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.stop_evil_twin_enterprise(&id) {
|
||||
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/pmkid") => {
|
||||
if !req.bool("confirm") {
|
||||
return err(400, "confirme a autorização do teste (confirm=true)");
|
||||
}
|
||||
let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else {
|
||||
return err(400, "iface e bssid são obrigatórios");
|
||||
};
|
||||
let attempts = req.u32("attempts").unwrap_or(5);
|
||||
match engine.pmkid_hunt(&iface, &bssid, attempts) {
|
||||
Ok(report) => ok(&report),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/pmf") => {
|
||||
if !req.bool("confirm") {
|
||||
return err(400, "confirme a autorização do teste (confirm=true)");
|
||||
}
|
||||
let (Some(iface), Some(bssid), Some(station)) =
|
||||
(req.text("iface"), req.text("bssid"), req.text("station"))
|
||||
else {
|
||||
return err(400, "iface, bssid e station são obrigatórios");
|
||||
};
|
||||
let reason = req.u16("reason").unwrap_or(7);
|
||||
match engine.pmf_test(&iface, &bssid, &station, reason) {
|
||||
Ok(report) => ok(&report),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/crack") => {
|
||||
let (Some(iface), Some(wordlist)) = (req.text("iface"), req.text("wordlist")) else {
|
||||
return err(400, "iface e wordlist são obrigatórios");
|
||||
let Some(iface) = req.text("iface") else {
|
||||
return err(400, "iface é obrigatória");
|
||||
};
|
||||
let bssid = req.text("bssid");
|
||||
let station = req.text("station");
|
||||
match engine.start_crack(&iface, bssid.as_deref(), station.as_deref(), &wordlist) {
|
||||
let rules = req.bool("rules");
|
||||
let result = match req.text("wordlist") {
|
||||
Some(wordlist) if !wordlist.trim().is_empty() => engine.start_crack(
|
||||
&iface,
|
||||
bssid.as_deref(),
|
||||
station.as_deref(),
|
||||
&wordlist,
|
||||
rules,
|
||||
),
|
||||
_ => {
|
||||
engine.start_crack_default(&iface, bssid.as_deref(), station.as_deref(), rules)
|
||||
}
|
||||
};
|
||||
match result {
|
||||
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/crack/mask") => {
|
||||
let Some(iface) = req.text("iface") else {
|
||||
return err(400, "iface é obrigatória");
|
||||
};
|
||||
let Some(mask) = req.text("mask").filter(|m| !m.trim().is_empty()) else {
|
||||
return err(400, "máscara é obrigatória");
|
||||
};
|
||||
let bssid = req.text("bssid");
|
||||
let station = req.text("station");
|
||||
let max_candidates = req.u64("max_candidates").unwrap_or(50_000_000);
|
||||
match engine.start_crack_mask(
|
||||
&iface,
|
||||
bssid.as_deref(),
|
||||
station.as_deref(),
|
||||
&mask,
|
||||
max_candidates,
|
||||
) {
|
||||
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
@@ -441,17 +711,94 @@ fn api(engine: &Arc<Engine>, req: &Request) -> (u16, String) {
|
||||
};
|
||||
let count = req.u32("count").unwrap_or(5);
|
||||
let reason = req.u16("reason").unwrap_or(7);
|
||||
match engine.deauth(
|
||||
let direction = req.text("direction").unwrap_or_else(|| "ap".into());
|
||||
let frame_kind = req.text("frame").unwrap_or_else(|| "deauth".into());
|
||||
match engine.deauth_directional(
|
||||
&iface,
|
||||
&bssid,
|
||||
req.text("station").as_deref(),
|
||||
count,
|
||||
reason,
|
||||
&direction,
|
||||
&frame_kind,
|
||||
) {
|
||||
Ok(report) => ok(&report),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/deauth/start") => {
|
||||
if !req.bool("confirm") {
|
||||
return err(
|
||||
400,
|
||||
"confirme a autorização do teste (confirm=true) antes de desautenticar",
|
||||
);
|
||||
}
|
||||
let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else {
|
||||
return err(400, "iface e bssid são obrigatórios");
|
||||
};
|
||||
let reason = req.u16("reason").unwrap_or(7);
|
||||
let direction = req.text("direction").unwrap_or_else(|| "both".into());
|
||||
let frame_kind = req.text("frame").unwrap_or_else(|| "deauth".into());
|
||||
let interval_ms = req.u64("interval_ms").unwrap_or(120);
|
||||
let max_secs = req.u64("max_secs");
|
||||
let stop_on_handshake = req.bool("stop_on_handshake");
|
||||
match engine.start_deauth_job(
|
||||
&iface,
|
||||
&bssid,
|
||||
req.text("station").as_deref(),
|
||||
reason,
|
||||
&direction,
|
||||
&frame_kind,
|
||||
interval_ms,
|
||||
max_secs,
|
||||
stop_on_handshake,
|
||||
) {
|
||||
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/deauth/stop") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.stop_deauth_job(&id) {
|
||||
Ok(()) => ok_json(serde_json::json!({ "ok": true })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("GET", "/api/deauth/job") => {
|
||||
let Some(id) = req.text("id") else {
|
||||
return err(400, "id obrigatório");
|
||||
};
|
||||
match engine.deauth_job(&id) {
|
||||
Some(job) => ok(&job),
|
||||
None => err(404, "job não encontrado"),
|
||||
}
|
||||
}
|
||||
("GET", "/api/deauth/jobs") => {
|
||||
let jobs = engine.deauth_jobs();
|
||||
match req.text("iface") {
|
||||
Some(iface) => ok(&jobs
|
||||
.into_iter()
|
||||
.filter(|j| j.iface.eq_ignore_ascii_case(&iface))
|
||||
.collect::<Vec<_>>()),
|
||||
None => ok(&jobs),
|
||||
}
|
||||
}
|
||||
("POST", "/api/handshakes/capture") => {
|
||||
if !req.bool("confirm") {
|
||||
return err(400, "confirme a autorização do teste (confirm=true)");
|
||||
}
|
||||
let (Some(iface), Some(bssid)) = (req.text("iface"), req.text("bssid")) else {
|
||||
return err(400, "iface e bssid são obrigatórios");
|
||||
};
|
||||
let seconds = req.u64("seconds");
|
||||
match engine.capture_handshake(&iface, &bssid, req.text("station").as_deref(), seconds)
|
||||
{
|
||||
Ok(id) => ok_json(serde_json::json!({ "ok": true, "job": id })),
|
||||
Err(e) => err(400, e),
|
||||
}
|
||||
}
|
||||
("POST", "/api/analyze") => {
|
||||
let pcap = if !req.body.is_empty() {
|
||||
serde_json::from_slice::<AnalyzeBody>(&req.body)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sabia-core"
|
||||
description = "Sabiá: núcleo de captura, análise e auditoria de redes Wi-Fi"
|
||||
description = "Sabiá: Wi-Fi network capture, analysis, and auditing core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -18,10 +18,12 @@ sha2 = "0.10"
|
||||
sha1 = "0.10"
|
||||
hmac = "0.12"
|
||||
md-5 = "0.10"
|
||||
aes = "0.8"
|
||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
[target.'cfg(not(target_os = "linux"))'.dependencies]
|
||||
# captura crua (AF_PACKET) só existe no Linux; outras plataformas compilam
|
||||
# a crate mas os módulos de captura ficam indisponíveis em tempo de execução.
|
||||
# Raw capture (AF_PACKET) is Linux-only; other platforms compile the crate
|
||||
# while live capture modules remain unavailable at runtime.
|
||||
@@ -0,0 +1,100 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: reaver-wps-fork-t6x
|
||||
Source: https://github.com/t6x/reaver-wps-fork-t6x
|
||||
|
||||
Files: *
|
||||
Copyright: 2011, Tactical Network Solutions, Craig Heffner <cheffner@tacnetsol.com>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/common/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/crypto/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi> and contributors
|
||||
License: GPL-2
|
||||
|
||||
Files: src/tls/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/tls/libtommath.c
|
||||
Copyright: Tom St Denis
|
||||
License: public-domain
|
||||
|
||||
Files: src/utils/*
|
||||
Copyright: 2002-2010, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/utils/radiotap.h
|
||||
Copyright: 2003-2004 David Young
|
||||
License: BSD-3-clause
|
||||
|
||||
Files: src/wps/*
|
||||
Copyright: 2007-2009, Jouni Malinen <j@w1.fi>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/wps/wps_ufd.c
|
||||
Copyright: 2009, Masashi Honma <honma@ictec.co.jp>
|
||||
License: GPL-2
|
||||
|
||||
Files: src/lwe/*
|
||||
Copyright: 1997-2007, Jean Tourrilhes <jt@hpl.hp.com>
|
||||
License: GPL-2
|
||||
|
||||
Files: debian/*
|
||||
Copyright: 2011-2018 Bartosz Fenski <fenio@debian.org>
|
||||
2023 Leandro Cunha <leandrocunha016@gmail.com>
|
||||
License: GPL-2
|
||||
|
||||
License: GPL-2
|
||||
This package is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this package; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License can be found in `/usr/share/common-licenses/GPL-2'.
|
||||
|
||||
License: BSD-3-clause
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
.
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
.
|
||||
3. Neither the name of the ORGANIZATION nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
License: public-domain
|
||||
The files tagged with this license contains the following paragraphs:
|
||||
.
|
||||
No copyright is claimed. This code is in the public domain; do with
|
||||
it what you wish.
|
||||
.
|
||||
Written by Karel Zak <kzak@redhat.com>
|
||||
@@ -0,0 +1,31 @@
|
||||
wpa-full.txt.gz
|
||||
|
||||
Dois dicionários concatenados, com as duplicatas removidas e a ordem de
|
||||
prioridade preservada (as entradas mais prováveis ficam primeiro, então a
|
||||
quebra por dicionário tende a achar a senha mais cedo em vez de só no fim):
|
||||
|
||||
1. Probable-Wordlists (berzerk0), Real-Passwords/WPA-Length/
|
||||
Top204Thousand-WPA-probable-v2.txt — 203.806 senhas reais vazadas, já
|
||||
filtradas pro comprimento válido de WPA (8–63) e ordenadas por frequência
|
||||
observada no mundo real.
|
||||
https://github.com/berzerk0/Probable-Wordlists
|
||||
Licença: Creative Commons Attribution-ShareAlike 4.0 International
|
||||
(CC BY-SA 4.0) — https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
2. rockyou.txt — o dump de senhas em texto puro do vazamento da RockYou de
|
||||
2009 (~32 milhões de contas), aqui filtrado pro mesmo comprimento válido
|
||||
de WPA. É provavelmente o dicionário de senhas mais usado em segurança
|
||||
ofensiva — vem embutido por padrão até no Kali Linux
|
||||
(/usr/share/wordlists/rockyou.txt.gz). Redistribuído aqui via SecLists:
|
||||
https://github.com/danielmiessler/SecLists (repositório MIT; o conteúdo do
|
||||
próprio rockyou.txt é dado de vazamento, sem titular de direitos autorais
|
||||
que o licencie formalmente — é tratado como de uso livre pra pesquisa de
|
||||
segurança em toda a indústria, mesmo padrão que o SecLists e as
|
||||
distribuições Linux de pentest adotam).
|
||||
|
||||
Ao redistribuir este arquivo (ou um derivado dele), mantenha a atribuição ao
|
||||
Probable-Wordlists acima — é a parte que tem uma licença formal que exige
|
||||
isso (CC BY-SA 4.0).
|
||||
|
||||
Isso é atribuição e aviso de licença, não os termos legais completos da
|
||||
CC BY-SA 4.0.
|
||||