chore: initial project skeleton
Acauã: Network Detection & Threat Hunting, heuristic analysis over a rolling 7-day window using deterministic math (time series, graph theory, information theory) instead of opaque ML models. Third product in the Ciberfauna ecosystem, alongside Guará and Sabiá. Follows the ecosystem's established conventions: OpenWolf project memory, bilingual PT-BR/EN docs and UI, Gitea Actions CI/CD on the same self-hosted runners, Conventional Commits, SemVer, logo and productName handling, MIT OR Apache-2.0 licensing. Architecture decision (made with the project owner after a dedicated consultation, not the original blueprint's suggested stack): Rust throughout, not Python/FastAPI/React. Most of the statistics (Bowley skew, MADM, Shannon entropy, PCR, Z-score, novel-edge detection, pivot chains) reduces to SQL over an embedded DuckDB/Parquet store; only the spectral analysis and graph centrality need host-language code, and both have workable Rust crates. Documented in full, including six corrections to the original blueprint's math (Rayleigh/Schuster instead of a degenerate Lomb-Scargle over bare timestamps, an explicit sentinel for Bowley skew's Q3=Q1 case, Jensen-Shannon instead of unbounded KL, a dedicated long-connection detector, JA4 vs. JA4+ licensing caution, deterministic per-alert evidence), in docs/architecture.md. Skeleton only: crates/acaua-core has the four detection pillars, fusion, storage, zeek log parsing and the updater as typed stubs that compile and return a clear "skeleton" status rather than real detection logic — that is separate, future work. crates/acaua-cli has serve/analyze/doctor/update. apps/gui is a minimal Tauri shell. Also already includes the one-click desktop self-update mechanism shared with Guará and Sabiá (AppImage/.deb/Windows installer), even with no release published yet, for consistency across the ecosystem. No Gitea repository exists for this project yet; this is a local-only commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
No files matched your search
@@ -0,0 +1,13 @@
|
||||
.git
|
||||
target
|
||||
apps/gui/src-tauri
|
||||
docs
|
||||
scripts
|
||||
research
|
||||
data
|
||||
.acaua
|
||||
*.parquet
|
||||
*.duckdb
|
||||
.claude
|
||||
.wolf
|
||||
.superpowers
|
||||
@@ -0,0 +1,2 @@
|
||||
# Interface captured by the Compose Zeek service. Use only on an authorized network.
|
||||
ZEEK_INTERFACE=eth0
|
||||
@@ -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 Acauã version", placeholder: "Ubuntu 24.04 / acaua 0.1.0" }
|
||||
- type: input
|
||||
id: input
|
||||
attributes: { label: "Dataset or input", placeholder: "Zeek log directory, pcap-derived dataset, or synthetic fixture" }
|
||||
- type: textarea
|
||||
id: log
|
||||
attributes: { label: "Terminal output (acaua doctor / analyze)", render: shell }
|
||||
@@ -0,0 +1,9 @@
|
||||
## What changes and why
|
||||
|
||||
## Checklist
|
||||
- [ ] `cargo fmt --all` and `cargo clippy --workspace --all-targets` pass without warnings
|
||||
- [ ] `cargo test --workspace` passes
|
||||
- [ ] Detection changes carry deterministic evidence and fixed-order reductions
|
||||
- [ ] Commits follow Conventional Commits in English and code/comments are in English
|
||||
- [ ] CHANGELOG updated (if there is a user-visible change)
|
||||
- [ ] Documentation kept in the English/Portuguese pair
|
||||
@@ -0,0 +1,88 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
# Documentation-only or local agent state changes do not trigger CI jobs.
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
|
||||
pull_request:
|
||||
paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', 'scripts/release-upload.sh', '.gitea/workflows/release.yml']
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: -D warnings
|
||||
# The runner persists /opt/hostedtoolcache across jobs: rustup toolchains,
|
||||
# cargo registry, npm/apt caches, and target directories live there so nothing
|
||||
# is re-downloaded or rebuilt from scratch on every run. Each job uses its own
|
||||
# target directory so differing RUSTFLAGS do not invalidate another job's cache.
|
||||
CARGO_HOME: /opt/hostedtoolcache/cargo
|
||||
RUSTUP_HOME: /opt/hostedtoolcache/rustup
|
||||
NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Format and clippy
|
||||
runs-on: linux-arm64
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/acaua-lint
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
- name: UI translation coverage
|
||||
run: node scripts/check-ui-i18n.mjs
|
||||
- run: cargo fmt --all -- --check
|
||||
- run: cargo clippy --workspace --all-targets
|
||||
|
||||
# Linux-only runner (ARM64).
|
||||
test:
|
||||
name: Tests (Linux)
|
||||
runs-on: linux-arm64
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/acaua-test
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo test --workspace
|
||||
|
||||
audit:
|
||||
name: Dependency audit
|
||||
runs-on: linux-arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# rustsec/audit-check depends on GitHub's checks API. cargo-audit itself
|
||||
# needs nothing but the advisory database, and installing it once into the
|
||||
# persistent CARGO_HOME makes every later run skip compilation.
|
||||
- name: cargo audit
|
||||
run: |
|
||||
command -v cargo-audit >/dev/null || cargo install cargo-audit --locked
|
||||
cargo audit
|
||||
|
||||
gui:
|
||||
name: Check desktop app (Linux)
|
||||
runs-on: linux-arm64
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/acaua-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,126 @@
|
||||
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).
|
||||
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 "Acauã ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
|
||||
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
||||
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
||||
-H 'Content-Type: application/json' -d @release.json >/dev/null
|
||||
|
||||
cli:
|
||||
name: CLI (${{ matrix.platform }})
|
||||
needs: release
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/acaua-release-cli-${{ matrix.platform }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { platform: linux-aarch64, target: aarch64-unknown-linux-gnu, apt: "", runner: linux-arm64 }
|
||||
- { platform: linux-x86_64, target: x86_64-unknown-linux-gnu, apt: "gcc-x86-64-linux-gnu", runner: linux-amd64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
- name: Platform cross-linker
|
||||
if: ${{ matrix.apt != '' && matrix.runner != 'linux-amd64' }}
|
||||
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/cli-x86_64 ${{ matrix.apt }}
|
||||
- name: Build and package
|
||||
run: scripts/package-cli.sh "${{ gitea.ref_name }}" "${{ matrix.platform }}" "${{ matrix.target }}" | tee packages.txt
|
||||
- name: Upload to release
|
||||
env:
|
||||
TOKEN: ${{ gitea.token }}
|
||||
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||
run: scripts/release-upload.sh "${{ gitea.ref_name }}" $(cat packages.txt)
|
||||
|
||||
gui:
|
||||
name: Desktop app (${{ matrix.platform }})
|
||||
needs: release
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
CARGO_TARGET_DIR: /opt/hostedtoolcache/cargo-target/acaua-release-gui-${{ matrix.platform }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { platform: linux-aarch64, runner: linux-arm64 }
|
||||
- { platform: linux-x86_64, runner: linux-amd64 }
|
||||
- { platform: windows, runner: linux-amd64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Tauri dependencies (Linux aarch64)
|
||||
if: matrix.platform == 'linux-aarch64'
|
||||
timeout-minutes: 25
|
||||
run: scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
- name: Tauri dependencies (Linux x86_64)
|
||||
if: matrix.platform == 'linux-x86_64'
|
||||
timeout-minutes: 25
|
||||
run: |
|
||||
if [ "$(uname -m)" = aarch64 ]; then
|
||||
dpkg --add-architecture amd64
|
||||
. /etc/os-release
|
||||
if [ "${ID:-}" = ubuntu ]; then
|
||||
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
|
||||
grep -q '^Architectures:' /etc/apt/sources.list.d/ubuntu.sources || sed -i '/^Types: deb/a Architectures: arm64' /etc/apt/sources.list.d/ubuntu.sources
|
||||
printf 'Types: deb\nURIs: http://archive.ubuntu.com/ubuntu/\nSuites: %s %s-updates %s-security\nComponents: main universe\nArchitectures: amd64\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.sources
|
||||
elif [ -f /etc/apt/sources.list ]; then
|
||||
sed -i 's/^deb http/deb [arch=arm64] http/' /etc/apt/sources.list
|
||||
printf 'deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-updates main universe\ndeb [arch=amd64] http://archive.ubuntu.com/ubuntu/ %s-security main universe\n' "$VERSION_CODENAME" "$VERSION_CODENAME" "$VERSION_CODENAME" >/etc/apt/sources.list.d/amd64.list
|
||||
fi
|
||||
fi
|
||||
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-amd64 gcc-x86-64-linux-gnu g++-x86-64-linux-gnu pkg-config patchelf file squashfs-tools qemu-user libglib2.0-bin libwebkit2gtk-4.1-dev:amd64 libayatana-appindicator3-dev:amd64 librsvg2-dev:amd64
|
||||
else
|
||||
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-native libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
||||
fi
|
||||
- name: Cross-compilation tools (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
run: |
|
||||
scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/tauri-win nsis lld llvm clang
|
||||
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
|
||||
- name: Build and package
|
||||
run: scripts/build-gui.sh "${{ matrix.platform }}"
|
||||
- name: Upload to release
|
||||
env:
|
||||
TOKEN: ${{ gitea.token }}
|
||||
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
||||
run: scripts/release-upload.sh "${{ gitea.ref_name }}" dist-gui/*
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# Enforces Conventional Commits 1.0.0 and English-only commit messages.
|
||||
set -euo pipefail
|
||||
|
||||
msg_file="${1:?usage: commit-msg <file>}"
|
||||
python3 - "$msg_file" <<'PY'
|
||||
import re, sys
|
||||
from pathlib import Path
|
||||
|
||||
raw = Path(sys.argv[1]).read_text(encoding="utf-8")
|
||||
lines = [l for l in raw.splitlines() if not l.startswith("#")]
|
||||
while lines and not lines[0].strip():
|
||||
lines.pop(0)
|
||||
if not lines:
|
||||
sys.exit("commit-msg: empty commit message")
|
||||
|
||||
subject = lines[0].strip()
|
||||
if subject.startswith(("Merge ", "Revert \"")):
|
||||
sys.exit(0)
|
||||
|
||||
conv_re = re.compile(
|
||||
r"^(feat|fix|chore|ci|docs|refactor|test|perf|build|style|revert)(\([a-z0-9_./-]+\))?!?: .+$"
|
||||
)
|
||||
if not conv_re.match(subject):
|
||||
sys.exit(
|
||||
f"commit-msg: invalid Conventional Commit header: {subject!r}\n"
|
||||
"Expected: <type>[optional scope]: <description in English>\n"
|
||||
"Allowed types: feat, fix, chore, ci, docs, refactor, test, perf, build, style, revert"
|
||||
)
|
||||
|
||||
body = "\n".join(lines)
|
||||
cleaned = re.sub(r"\b(Guará|Sabiá|Acauã|Ciberfauna)\b", "", body)
|
||||
pt_accents = re.search(r"[áàâãéêíóôõúçÁÀÂÃÉÊÍÓÔÕÚÇ]", cleaned)
|
||||
pt_words = re.search(
|
||||
r"\b(não|para|como|quando|então|pelos|pelas|pelo|pela|estão|está|também|versão|"
|
||||
r"corrige|adiciona|atualiza|remove|padroniza|implementa|ajusta|arquivo|arquivos)\b",
|
||||
cleaned,
|
||||
re.I,
|
||||
)
|
||||
if pt_accents or pt_words:
|
||||
sys.exit("commit-msg: commit messages must be written 100% in English.")
|
||||
PY
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
exec "$(git rev-parse --show-toplevel)/scripts/check.sh"
|
||||
@@ -0,0 +1,42 @@
|
||||
# Build artifacts
|
||||
target/
|
||||
apps/gui/src-tauri/target/
|
||||
apps/gui/src-tauri/gen/
|
||||
node_modules/
|
||||
dist-gui/
|
||||
/acaua-v*
|
||||
*.rs.bk
|
||||
|
||||
# Runtime data and local datasets
|
||||
.acaua/
|
||||
data/
|
||||
*.parquet
|
||||
*.duckdb
|
||||
*.wal
|
||||
|
||||
# Python research environment
|
||||
research/.venv/
|
||||
research/**/__pycache__/
|
||||
research/**/*.pyc
|
||||
research/.ipynb_checkpoints/
|
||||
|
||||
# Local environment and editor state
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.DS_Store
|
||||
|
||||
# Local AI agent configuration and state
|
||||
.claude/
|
||||
.wolf/
|
||||
.opencode/
|
||||
.codex/
|
||||
.cursor/
|
||||
.superpowers/
|
||||
.superset/
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
GEMINI.md
|
||||
|
||||
# Logo copied into the UI directory by the desktop build script
|
||||
/apps/gui/ui/logo.png
|
||||
@@ -0,0 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
**English** · [Português](CHANGELOG.pt-BR.md)
|
||||
|
||||
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow SemVer.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-10-31
|
||||
|
||||
### Added
|
||||
- Rust workspace with `acaua-core` and `acaua-cli`.
|
||||
- CLI skeleton with `serve`, `analyze`, `doctor` and `update [--apply]`.
|
||||
- Axum API skeleton with health, status, alerts, analyze and update-check routes.
|
||||
- Embedded vanilla UI and Tauri desktop shell.
|
||||
- Deterministic pillar, fusion, storage, Zeek and updater stubs with smoke tests.
|
||||
- English/Portuguese documentation, CI/release workflows and local check script.
|
||||
|
||||
### Notes
|
||||
- The four mathematical pillars are not implemented.
|
||||
- The Zeek parser and DuckDB storage are stubs.
|
||||
- No release artifact has been published.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
[English](CHANGELOG.md) · **Português**
|
||||
|
||||
Formato baseado no [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); versões seguem SemVer.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0] - 2026-10-31
|
||||
|
||||
### Adicionado
|
||||
- Workspace Rust com `acaua-core` e `acaua-cli`.
|
||||
- Esqueleto da CLI com `serve`, `analyze`, `doctor` e `update [--apply]`.
|
||||
- Esqueleto da API Axum com rotas de health, status, alertas, análise e checagem de atualização.
|
||||
- UI vanilla embutida e shell desktop Tauri.
|
||||
- Stubs determinísticos de pilares, fusão, armazenamento, Zeek e updater com testes de fumaça.
|
||||
- Documentação em inglês/português, workflows de CI/release e script local de verificação.
|
||||
|
||||
### Notas
|
||||
- Os quatro pilares matemáticos não estão implementados.
|
||||
- O parser Zeek e o armazenamento DuckDB são stubs.
|
||||
- Nenhum artefato de release foi publicado.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Contributing to Acauã
|
||||
|
||||
**English** · [Português](CONTRIBUTING.pt-BR.md)
|
||||
|
||||
Thanks for helping. The project is a pre-alpha skeleton, so the most useful
|
||||
contributions right now are tests, fixtures, documentation and small,
|
||||
well-scoped design corrections.
|
||||
|
||||
1. **Authorized monitoring only.** Acauã is defensive software for networks you
|
||||
own or are explicitly authorized to observe. Do not add collection paths that
|
||||
hide scope, retention or evidence.
|
||||
2. **Evidence is part of the feature.** A detection change must state its
|
||||
inputs, fixed-order reductions and intermediate values. A score without a
|
||||
reproducible evidence record is not accepted.
|
||||
3. **Be honest about what works.** If a feature is a stub, Linux-only or limited
|
||||
to one Zeek log, say so in the README, the CLI help and the test.
|
||||
4. **100% English codebase.** Source code, comments, scripts, workflows and
|
||||
commit messages are English. The GUI and CLI remain bilingual (PT-BR and EN
|
||||
through `i18n-en.js`).
|
||||
|
||||
## Workflow
|
||||
|
||||
```bash
|
||||
scripts/install-hooks.sh
|
||||
scripts/check.sh
|
||||
scripts/check.sh --full
|
||||
```
|
||||
|
||||
`check.sh` runs formatting, clippy with warnings as errors, UI translation
|
||||
coverage, shell syntax, workflow YAML, SemVer consistency and the desktop check.
|
||||
`--full` adds workspace tests and `cargo audit`.
|
||||
|
||||
The optional DuckDB feature is not part of the default check:
|
||||
|
||||
```bash
|
||||
cargo check -p acaua-core --features duckdb
|
||||
```
|
||||
|
||||
## Research
|
||||
|
||||
`research/` is an isolated Python workspace for scientific validation. It may
|
||||
use astropy, scipy, networkx, pandas and notebooks. It must never be imported by
|
||||
the Rust runtime, copied into a release or required to build the product.
|
||||
|
||||
## Detection rules
|
||||
|
||||
- Keep reductions in a fixed order when floating-point results are part of an
|
||||
alert.
|
||||
- Use a documented sentinel instead of `NaN` or a panic for degenerate inputs.
|
||||
- Use Jensen-Shannon, not raw KL, when a distribution can contain zero
|
||||
probability.
|
||||
- Do not use JA4S/JA4H/JA4X/JA4SSH/JA4L by default: those variants carry a
|
||||
non-OSI license. The client TLS JA4 fingerprint is the only default.
|
||||
- Long connections need their own detector; they do not produce the short
|
||||
intervals a beaconing score expects.
|
||||
|
||||
## Interface and translation
|
||||
|
||||
The interface source text is Portuguese. English comes from
|
||||
`apps/gui/ui/i18n-en.js`, applied at runtime by `i18n.js`. When adding visible
|
||||
text, run `node scripts/check-ui-i18n.mjs` and include the English pair.
|
||||
|
||||
## Documentation
|
||||
|
||||
Keep the pair: every `*.md` and `docs/*` file has an English version and a
|
||||
Portuguese version (`*.pt-BR.md`, `docs/pt-BR/`). File names are translated only
|
||||
when the word actually changes, as in `architecture.md` → `arquitetura.md`.
|
||||
|
||||
## Commits and releases
|
||||
|
||||
Commits follow Conventional Commits 1.0.0 in English (`feat:`, `fix:`, `chore:`,
|
||||
`ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`). Versions follow SemVer
|
||||
2.0.0 and are synchronized across the workspace, the desktop manifest and the
|
||||
Tauri config.
|
||||
|
||||
Pushing a `v*` tag creates a Gitea Release from the matching `CHANGELOG.md`
|
||||
section and attaches the Linux CLI packages plus the desktop bundles.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree to license under [MIT](LICENSE-MIT) **or**
|
||||
[Apache-2.0](LICENSE-APACHE), like the rest of the project.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Contribuindo com o Acauã
|
||||
|
||||
[English](CONTRIBUTING.md) · **Português**
|
||||
|
||||
Obrigado por ajudar. O projeto é um esqueleto pré-alfa, então as contribuições
|
||||
mais úteis agora são testes, fixtures, documentação e correções de projeto
|
||||
pequenas e bem delimitadas.
|
||||
|
||||
1. **Só monitoramento autorizado.** O Acauã é software defensivo para redes
|
||||
próprias ou com autorização explícita. Não adicione caminhos de coleta que
|
||||
escondam escopo, retenção ou evidência.
|
||||
2. **Evidência faz parte do recurso.** Uma mudança de detecção precisa declarar
|
||||
entradas, reduções em ordem fixa e valores intermediários. Pontuação sem
|
||||
registro reproduzível de evidência não é aceita.
|
||||
3. **Seja honesto sobre o que funciona.** Se um recurso é stub, só roda no Linux
|
||||
ou cobre apenas um log Zeek, diga isso no README, no `--help` e no teste.
|
||||
4. **Código 100% em inglês.** Código-fonte, comentários, scripts, workflows e
|
||||
mensagens de commit são em inglês. A GUI e a CLI continuam bilíngues (PT-BR e
|
||||
EN via `i18n-en.js`).
|
||||
|
||||
## Fluxo
|
||||
|
||||
```bash
|
||||
scripts/install-hooks.sh
|
||||
scripts/check.sh
|
||||
scripts/check.sh --full
|
||||
```
|
||||
|
||||
O `check.sh` roda formatação, clippy com warnings como erro, cobertura de
|
||||
tradução da UI, sintaxe de shell, YAML dos workflows, consistência de SemVer e a
|
||||
checagem do desktop. O `--full` inclui testes do workspace e `cargo audit`.
|
||||
|
||||
A feature opcional DuckDB não entra na checagem padrão:
|
||||
|
||||
```bash
|
||||
cargo check -p acaua-core --features duckdb
|
||||
```
|
||||
|
||||
## Pesquisa
|
||||
|
||||
`research/` é um espaço Python isolado para validação científica. Ele pode usar
|
||||
astropy, scipy, networkx, pandas e notebooks. Ele nunca deve ser importado pelo
|
||||
runtime Rust, copiado para um release ou exigido para compilar o produto.
|
||||
|
||||
## Regras de detecção
|
||||
|
||||
- Mantenha reduções em ordem fixa quando resultados de ponto flutuante entram em
|
||||
um alerta.
|
||||
- Use um valor sentinela documentado em vez de `NaN` ou panic para entradas
|
||||
degeneradas.
|
||||
- Use Jensen-Shannon, não KL puro, quando uma distribuição pode ter probabilidade
|
||||
zero.
|
||||
- Não use JA4S/JA4H/JA4X/JA4SSH/JA4L por padrão: essas variantes têm licença
|
||||
não-OSI. O JA4 de impressão digital de cliente TLS é o único padrão.
|
||||
- Conexões longas precisam de detector próprio; elas não produzem os intervalos
|
||||
curtos que uma pontuação de beaconing espera.
|
||||
|
||||
## Interface e tradução
|
||||
|
||||
O texto-fonte da interface é em português. O inglês vem de
|
||||
`apps/gui/ui/i18n-en.js`, aplicado em tempo de execução por `i18n.js`. Ao
|
||||
adicionar texto visível, rode `node scripts/check-ui-i18n.mjs` e inclua o par em
|
||||
inglês.
|
||||
|
||||
## Documentação
|
||||
|
||||
Mantenha o par: todo arquivo `*.md` e `docs/*` tem versão em inglês e em
|
||||
português (`*.pt-BR.md`, `docs/pt-BR/`). Nomes de arquivo só são traduzidos
|
||||
quando a palavra muda de verdade, como `architecture.md` → `arquitetura.md`.
|
||||
|
||||
## Commits e releases
|
||||
|
||||
Os commits seguem Conventional Commits 1.0.0 em inglês (`feat:`, `fix:`,
|
||||
`chore:`, `ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`). As versões
|
||||
seguem SemVer 2.0.0 e ficam sincronizadas entre o workspace, o manifesto do
|
||||
desktop e a configuração do Tauri.
|
||||
|
||||
Enviar uma tag `v*` cria uma Release no Gitea a partir da seção correspondente do
|
||||
`CHANGELOG.md` e anexa os pacotes Linux da CLI e os bundles do desktop.
|
||||
|
||||
## Licença
|
||||
|
||||
Ao contribuir, você concorda em licenciar sob [MIT](LICENSE-MIT) **ou**
|
||||
[Apache-2.0](LICENSE-APACHE), como o resto do projeto.
|
||||
@@ -0,0 +1,22 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/acaua-core", "crates/acaua-cli"]
|
||||
exclude = ["apps/gui/src-tauri", "research"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://git.lucasrguerra.dev.br/ciberfauna/acaua"
|
||||
authors = ["Acauã contributors"]
|
||||
rust-version = "1.80"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
@@ -0,0 +1,23 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM rust:1-slim-bookworm AS build
|
||||
WORKDIR /src
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY crates crates
|
||||
COPY apps/gui/ui apps/gui/ui
|
||||
COPY logo.png logo.png
|
||||
RUN cargo build --release --locked -p acaua-cli
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates curl tar \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=build /src/target/release/acaua /usr/local/bin/acaua
|
||||
LABEL org.opencontainers.image.title="Acauã" \
|
||||
org.opencontainers.image.description="Network threat hunting with deterministic evidence" \
|
||||
org.opencontainers.image.source="https://git.lucasrguerra.dev.br/lucasrguerra/acaua" \
|
||||
org.opencontainers.image.licenses="MIT OR Apache-2.0"
|
||||
WORKDIR /data
|
||||
EXPOSE 8787
|
||||
ENTRYPOINT ["acaua"]
|
||||
CMD ["serve", "--bind", "0.0.0.0:8787", "--data-dir", "/data/acaua"]
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Acauã 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,145 @@
|
||||
<p align="center"><img src="logo.png" width="300" alt="Acauã"></p>
|
||||
<h1 align="center">Acauã</h1>
|
||||
<p align="center">Deterministic network threat hunting.<br>Zeek-first NDR research · local-first · 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="docs/docker.md">Docker</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>
|
||||
|
||||
---
|
||||
|
||||
**Status: pre-alpha skeleton.** The project structure, documentation, CI and
|
||||
compiling stubs exist. The four mathematical detection pillars, the real Zeek
|
||||
parser, DuckDB storage and the alert engine are **under development** and are
|
||||
not shipped yet. Nothing on this page should be read as a claim that detection
|
||||
already works.
|
||||
|
||||
Acauã is a network threat hunting engine built around deterministic evidence,
|
||||
not an opaque model. The goal is to answer not only "this looks suspicious" but
|
||||
"these exact inputs, with these intermediate values, produced this score".
|
||||
|
||||
- **Deterministic by design:** fixed-order reductions, documented sentinels and
|
||||
a JSON evidence record for every future alert.
|
||||
- **Local-first:** the analytical store is embedded in the process; no external
|
||||
database and no telemetry by default.
|
||||
- **Zeek-first:** `dns.log`, `ssl.log`, `conn.log` and the protocol logs needed
|
||||
for identity and lateral-movement analysis. NetFlow/IPFIX is a documented
|
||||
degraded mode, never presented as equivalent. Suricata EVE JSON is an optional
|
||||
secondary input.
|
||||
- **Daemon and desktop:** `acaua serve` runs continuous ingestion, scheduling,
|
||||
the API and the embedded UI; the Tauri app opens a local dataset or talks to a
|
||||
remote `serve`.
|
||||
- **Research stays outside the product:** `research/` is an isolated Python
|
||||
workspace for scientific validation and fixtures. It is never compiled,
|
||||
packaged or released.
|
||||
|
||||
## What exists today
|
||||
|
||||
| Area | State |
|
||||
|---|---|
|
||||
| Rust workspace and CLI stubs | ✅ compiles |
|
||||
| Axum API with health, status, alerts and analyze routes | ✅ stub |
|
||||
| Desktop shell and vanilla UI | ✅ shell, no real screenshots |
|
||||
| Documentation and CI/release workflows | ✅ |
|
||||
| Four detection pillars | 🚧 under development |
|
||||
| Zeek log parser | 🚧 stub |
|
||||
| DuckDB/Parquet storage and 7-day TTL | 🚧 reserved, feature disabled by default |
|
||||
| Alert evidence and scoring | 🚧 stub |
|
||||
| Published releases | ❌ none yet |
|
||||
|
||||
## The four pillars
|
||||
|
||||
All four are **under development**. The roadmap keeps them as future work, not
|
||||
as implemented features.
|
||||
|
||||
1. **Beaconing:** periodicity on event timestamps with a Rayleigh/Schuster
|
||||
periodogram, and generalized Lomb–Scargle only where a real `y` exists.
|
||||
2. **Persistent sessions:** long connections that never produce a short `Δt`
|
||||
and would otherwise stay invisible to a beaconing detector.
|
||||
3. **Protocol anomalies:** deterministic features over Zeek protocol logs,
|
||||
including identity and lateral-movement signals.
|
||||
4. **Evidence fusion:** bounded, symmetric combination of pillar scores with a
|
||||
reproducible evidence record per alert.
|
||||
|
||||
The design decisions behind these pillars, including the six corrections to the
|
||||
original blueprint, are in **[docs/architecture.md](docs/architecture.md)**.
|
||||
|
||||
## Authorized use
|
||||
|
||||
Acauã is defensive software for networks you own or are explicitly authorized
|
||||
to monitor. Capturing or analyzing third-party traffic without permission may
|
||||
be illegal. The operator is responsible for scope, privacy and retention.
|
||||
|
||||
The daemon binds to `127.0.0.1` by default. The optional Docker deployment is
|
||||
documented for lab and research use.
|
||||
|
||||
## Install
|
||||
|
||||
There are no published releases yet. Build from source:
|
||||
|
||||
```bash
|
||||
cargo build --release -p acaua-cli
|
||||
./target/release/acaua --help
|
||||
```
|
||||
|
||||
Rust 1.80+ is required. The desktop app needs the Tauri system dependencies for
|
||||
your platform.
|
||||
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
acaua doctor
|
||||
acaua serve --bind 127.0.0.1:8787 --data-dir .acaua
|
||||
acaua analyze /var/log/zeek/current --json
|
||||
acaua update
|
||||
```
|
||||
|
||||
`analyze` currently validates the directory and returns a skeleton report; it
|
||||
does not parse Zeek logs yet. `serve` exposes the stub API and the embedded UI.
|
||||
`update` uses the same Gitea Releases + SHA-256 model as Guará and Sabiá.
|
||||
|
||||
## Desktop app
|
||||
|
||||
The Tauri app is a shell for the same UI used by `serve`. It has a dashboard for
|
||||
the four pillars, a dataset path field, an alerts panel and settings for language
|
||||
and theme. It does **not** render detection results yet. See
|
||||
**[docs/interface.md](docs/interface.md)**.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
node scripts/check-ui-i18n.mjs
|
||||
scripts/check.sh
|
||||
```
|
||||
|
||||
The optional DuckDB backend is compiled with `--features duckdb`; it is disabled
|
||||
by default so the skeleton stays fast to check. See
|
||||
**[CONTRIBUTING.md](CONTRIBUTING.md)** and
|
||||
**[docs/architecture.md](docs/architecture.md)**.
|
||||
|
||||
## Known limits
|
||||
|
||||
- The detection engine, Zeek parser and storage layer are stubs.
|
||||
- There is no published binary, installer or release asset.
|
||||
- The GUI has no real screenshots because there is nothing to render yet.
|
||||
- NetFlow/IPFIX is a degraded input mode by design; it cannot replace the Zeek
|
||||
protocol logs for every pillar.
|
||||
- The project targets Linux for the daemon and the CI release pipeline. The
|
||||
desktop app follows the same Tauri platform matrix as the sibling projects.
|
||||
|
||||
## Where it's going
|
||||
|
||||
Summary of the [ROADMAP](ROADMAP.md):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Done** | Workspace, CLI/API stubs, GUI shell, documentation, CI/release workflows |
|
||||
| **Next** | Pillar 1 and 2 implementations with deterministic fixtures; real Zeek parser |
|
||||
| **Later** | DuckDB/Parquet storage, TTL, pillar 3 and 4, evidence fusion, desktop dataset view |
|
||||
| **Not planned** | Opaque AI scoring, a required cloud service, Python in the shipped runtime |
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), your choice.
|
||||
@@ -0,0 +1,147 @@
|
||||
<p align="center"><img src="logo.png" width="300" alt="Acauã"></p>
|
||||
<h1 align="center">Acauã</h1>
|
||||
<p align="center">Caça determinística a ameaças em rede.<br>NDR com Zeek como entrada principal · local-first · 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="docs/pt-BR/docker.md">Docker</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>
|
||||
|
||||
---
|
||||
|
||||
**Estado: esqueleto pré-alfa.** A estrutura do projeto, a documentação, o CI e
|
||||
os stubs que compilam existem. Os quatro pilares matemáticos de detecção, o
|
||||
parser real de logs Zeek, o armazenamento DuckDB e o motor de alertas estão
|
||||
**em desenvolvimento** e ainda não são distribuídos. Nada nesta página deve ser
|
||||
lido como promessa de que a detecção já funciona.
|
||||
|
||||
O Acauã é um motor de caça a ameaças em rede construído em torno de evidência
|
||||
determinística, não de um modelo opaco. O objetivo é responder não só "isto
|
||||
parece suspeito", mas "estas entradas exatas, com estes valores intermediários,
|
||||
produziram esta pontuação".
|
||||
|
||||
- **Determinístico por projeto:** reduções em ordem fixa, sentinelas
|
||||
documentados e um registro JSON de evidência para cada alerta futuro.
|
||||
- **Local-first:** o armazenamento analítico mora dentro do processo; nenhum
|
||||
banco externo e nenhuma telemetria por padrão.
|
||||
- **Zeek como entrada principal:** `dns.log`, `ssl.log`, `conn.log` e os logs de
|
||||
protocolo necessários para identidade e movimento lateral. NetFlow/IPFIX é um
|
||||
modo degradado documentado, nunca apresentado como equivalente. Suricata EVE
|
||||
JSON é uma entrada secundária opcional.
|
||||
- **Daemon e desktop:** `acaua serve` roda ingestão contínua, agendador, API e a
|
||||
UI embutida; o app Tauri abre um dataset local ou conversa com um `serve`
|
||||
remoto.
|
||||
- **Pesquisa fora do produto:** `research/` é um espaço Python isolado para
|
||||
validação científica e fixtures. Ele nunca é compilado, empacotado ou
|
||||
distribuído.
|
||||
|
||||
## O que existe hoje
|
||||
|
||||
| Área | Estado |
|
||||
|---|---|
|
||||
| Workspace Rust e stubs da CLI | ✅ compila |
|
||||
| API Axum com rotas de health, status, alertas e análise | ✅ stub |
|
||||
| Shell desktop e UI vanilla | ✅ shell, sem screenshots reais |
|
||||
| Documentação e workflows de CI/release | ✅ |
|
||||
| Quatro pilares de detecção | 🚧 em desenvolvimento |
|
||||
| Parser de logs Zeek | 🚧 stub |
|
||||
| Armazenamento DuckDB/Parquet e TTL de 7 dias | 🚧 reservado, feature desligada por padrão |
|
||||
| Evidência e pontuação de alertas | 🚧 stub |
|
||||
| Releases publicadas | ❌ nenhuma ainda |
|
||||
|
||||
## Os quatro pilares
|
||||
|
||||
Todos os quatro estão **em desenvolvimento**. O roadmap os mantém como trabalho
|
||||
futuro, não como recurso implementado.
|
||||
|
||||
1. **Beaconing:** periodicidade sobre timestamps de eventos com periodograma de
|
||||
Rayleigh/Schuster, e Lomb–Scargle generalizado só onde existe um `y` real.
|
||||
2. **Sessões persistentes:** conexões longas que nunca produzem um `Δt` curto e
|
||||
ficariam invisíveis para um detector de beaconing.
|
||||
3. **Anomalias de protocolo:** features determinísticas sobre logs de protocolo
|
||||
do Zeek, incluindo sinais de identidade e movimento lateral.
|
||||
4. **Fusão de evidências:** combinação limitada e simétrica das pontuações dos
|
||||
pilares, com um registro de evidência reproduzível por alerta.
|
||||
|
||||
As decisões de projeto por trás desses pilares, incluindo as seis correções ao
|
||||
blueprint original, estão em **[docs/pt-BR/arquitetura.md](docs/pt-BR/arquitetura.md)**.
|
||||
|
||||
## Uso autorizado
|
||||
|
||||
O Acauã é software defensivo para redes próprias ou para redes que você tem
|
||||
autorização explícita para monitorar. Capturar ou analisar tráfego de terceiros
|
||||
sem permissão pode ser ilegal. O operador é responsável por escopo, privacidade e
|
||||
retenção.
|
||||
|
||||
O daemon escuta em `127.0.0.1` por padrão. A implantação opcional com Docker é
|
||||
documentada para laboratório e pesquisa.
|
||||
|
||||
## Instalação
|
||||
|
||||
Ainda não há releases publicadas. Compile do código:
|
||||
|
||||
```bash
|
||||
cargo build --release -p acaua-cli
|
||||
./target/release/acaua --help
|
||||
```
|
||||
|
||||
É preciso Rust 1.80+. O app desktop precisa das dependências de sistema do Tauri
|
||||
para a sua plataforma.
|
||||
|
||||
## Começando
|
||||
|
||||
```bash
|
||||
acaua doctor
|
||||
acaua serve --bind 127.0.0.1:8787 --data-dir .acaua
|
||||
acaua analyze /var/log/zeek/current --json
|
||||
acaua update
|
||||
```
|
||||
|
||||
O `analyze` hoje valida o diretório e devolve um relatório de esqueleto; ele
|
||||
ainda não lê logs Zeek. O `serve` expõe a API stub e a UI embutida. O `update`
|
||||
usa o mesmo modelo de Gitea Releases + SHA-256 do Guará e do Sabiá.
|
||||
|
||||
## App desktop
|
||||
|
||||
O app Tauri é um shell para a mesma UI usada pelo `serve`. Ele tem um painel dos
|
||||
quatro pilares, um campo de caminho de dataset, um painel de alertas e
|
||||
configurações de idioma e tema. Ele **ainda não** renderiza resultados de
|
||||
detecção. Veja **[docs/pt-BR/interface.md](docs/pt-BR/interface.md)**.
|
||||
|
||||
## Desenvolvimento
|
||||
|
||||
```bash
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets
|
||||
node scripts/check-ui-i18n.mjs
|
||||
scripts/check.sh
|
||||
```
|
||||
|
||||
O backend DuckDB opcional é compilado com `--features duckdb`; ele fica desligado
|
||||
por padrão para o esqueleto continuar rápido de verificar. Veja
|
||||
**[CONTRIBUTING.pt-BR.md](CONTRIBUTING.pt-BR.md)** e
|
||||
**[docs/pt-BR/arquitetura.md](docs/pt-BR/arquitetura.md)**.
|
||||
|
||||
## Limites conhecidos
|
||||
|
||||
- O motor de detecção, o parser Zeek e a camada de armazenamento são stubs.
|
||||
- Não há binário, instalador ou asset de release publicado.
|
||||
- A GUI não tem screenshots reais porque ainda não há nada para renderizar.
|
||||
- NetFlow/IPFIX é um modo degradado por projeto; ele não substitui os logs de
|
||||
protocolo do Zeek em todos os pilares.
|
||||
- O projeto mira Linux para o daemon e para o pipeline de release do CI. O app
|
||||
desktop segue a mesma matriz Tauri dos projetos irmãos.
|
||||
|
||||
## Para onde vai
|
||||
|
||||
Resumo do [ROADMAP](ROADMAP.pt-BR.md):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Feito** | Workspace, stubs de CLI/API, shell da GUI, documentação, workflows de CI/release |
|
||||
| **Próximo** | Implementação dos pilares 1 e 2 com fixtures determinísticas; parser Zeek real |
|
||||
| **Depois** | Armazenamento DuckDB/Parquet, TTL, pilares 3 e 4, fusão de evidências, visão de dataset no desktop |
|
||||
| **Fora de plano** | Pontuação por IA opaca, serviço de nuvem obrigatório, Python no runtime distribuído |
|
||||
|
||||
## Licença
|
||||
|
||||
[MIT](LICENSE-MIT) ou [Apache-2.0](LICENSE-APACHE), à sua escolha.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Roadmap
|
||||
|
||||
**English** · [Português](ROADMAP.pt-BR.md)
|
||||
|
||||
This roadmap separates what exists from what is planned. The four pillars are
|
||||
**not implemented** in `0.1.0`; they are the next body of work.
|
||||
|
||||
## Done
|
||||
|
||||
- Rust workspace with `acaua-core` and `acaua-cli`.
|
||||
- CLI skeleton, Axum API skeleton and embedded vanilla UI.
|
||||
- Tauri desktop shell with language and theme settings.
|
||||
- Stubs for the four pillars, evidence fusion, storage, Zeek ingestion and the
|
||||
Gitea-based updater.
|
||||
- English/Portuguese documentation, CI/release workflows and `scripts/check.sh`.
|
||||
- Deterministic smoke tests and a bilingual UI check.
|
||||
|
||||
## Next
|
||||
|
||||
1. **Pillar 1 — beaconing.** Implement the Rayleigh/Schuster periodogram on
|
||||
event timestamps, add generalized Lomb–Scargle only when a real `y` exists,
|
||||
and ship golden fixtures with fixed expected values.
|
||||
2. **Pillar 2 — persistent sessions.** Detect long connections that never
|
||||
produce a short interval, with explicit thresholds and no overlap with
|
||||
pillar 1.
|
||||
3. **Real Zeek parser.** Read `conn.log`, `dns.log`, `ssl.log` and protocol logs
|
||||
with bounded memory, path confinement and deterministic ordering.
|
||||
4. **Evidence record.** Emit a JSON record per alert with inputs, intermediate
|
||||
values, code version and dataset hash.
|
||||
|
||||
## Later
|
||||
|
||||
- Pillar 3 — deterministic protocol anomalies, including identity and
|
||||
lateral-movement features.
|
||||
- Pillar 4 — bounded, symmetric fusion with reproducible weights.
|
||||
- DuckDB/Parquet storage behind the optional `duckdb` feature, with a seven-day
|
||||
TTL and an export command.
|
||||
- Desktop dataset view, alert detail with evidence, and scheduled `serve` runs.
|
||||
- NetFlow/IPFIX as a documented degraded mode and Suricata EVE JSON as an
|
||||
optional secondary input.
|
||||
- Signed release metadata, SBOM and reproducible builds.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Opaque AI scoring that cannot explain an alert.
|
||||
- A required cloud service or telemetry pipeline.
|
||||
- Python in the shipped runtime.
|
||||
- Treating flow records as equivalent to full Zeek protocol logs.
|
||||
- Automatic desktop self-update.
|
||||
|
||||
## Versioning
|
||||
|
||||
The project follows SemVer 2.0.0. `0.x` means the API and detection output may
|
||||
change between minor versions. A `1.0.0` release will require stable evidence
|
||||
format, documented thresholds and a migration path for stored data.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Roadmap
|
||||
|
||||
[English](ROADMAP.md) · **Português**
|
||||
|
||||
Este roadmap separa o que existe do que está planejado. Os quatro pilares **não
|
||||
estão implementados** em `0.1.0`; eles são o próximo bloco de trabalho.
|
||||
|
||||
## Feito
|
||||
|
||||
- Workspace Rust com `acaua-core` e `acaua-cli`.
|
||||
- Esqueleto da CLI, esqueleto da API Axum e UI vanilla embutida.
|
||||
- Shell desktop Tauri com configurações de idioma e tema.
|
||||
- Stubs dos quatro pilares, fusão de evidências, armazenamento, ingestão Zeek e
|
||||
updater baseado em Gitea.
|
||||
- Documentação em inglês/português, workflows de CI/release e `scripts/check.sh`.
|
||||
- Testes de fumaça determinísticos e checagem bilíngue da UI.
|
||||
|
||||
## Próximo
|
||||
|
||||
1. **Pilar 1 — beaconing.** Implementar o periodograma de Rayleigh/Schuster
|
||||
sobre timestamps de eventos, adicionar Lomb–Scargle generalizado só quando
|
||||
existir um `y` real e entregar fixtures douradas com valores esperados fixos.
|
||||
2. **Pilar 2 — sessões persistentes.** Detectar conexões longas que nunca
|
||||
produzem um intervalo curto, com limiares explícitos e sem sobreposição com o
|
||||
pilar 1.
|
||||
3. **Parser Zeek real.** Ler `conn.log`, `dns.log`, `ssl.log` e logs de
|
||||
protocolo com memória limitada, confinamento de caminho e ordem determinística.
|
||||
4. **Registro de evidência.** Emitir um JSON por alerta com entradas, valores
|
||||
intermediários, versão do código e hash do dataset.
|
||||
|
||||
## Depois
|
||||
|
||||
- Pilar 3 — anomalias determinísticas de protocolo, incluindo identidade e
|
||||
movimento lateral.
|
||||
- Pilar 4 — fusão limitada e simétrica, com pesos reproduzíveis.
|
||||
- Armazenamento DuckDB/Parquet atrás da feature opcional `duckdb`, com TTL de
|
||||
sete dias e comando de exportação.
|
||||
- Visão de dataset no desktop, detalhe de alerta com evidência e execuções
|
||||
agendadas do `serve`.
|
||||
- NetFlow/IPFIX como modo degradado documentado e Suricata EVE JSON como entrada
|
||||
secundária opcional.
|
||||
- Metadados de release assinados, SBOM e builds reproduzíveis.
|
||||
|
||||
## Fora de plano
|
||||
|
||||
- Pontuação por IA opaca que não explica um alerta.
|
||||
- Serviço de nuvem obrigatório ou pipeline de telemetria.
|
||||
- Python no runtime distribuído.
|
||||
- Tratar registros de fluxo como equivalentes a logs completos de protocolo Zeek.
|
||||
- Auto-atualização automática do desktop.
|
||||
|
||||
## Versionamento
|
||||
|
||||
O projeto segue SemVer 2.0.0. `0.x` significa que a API e a saída de detecção
|
||||
podem mudar entre versões menores. Uma versão `1.0.0` vai exigir formato de
|
||||
evidência estável, limiares documentados e caminho de migração para dados
|
||||
armazenados.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Security policy
|
||||
|
||||
**English** · [Português](SECURITY.pt-BR.md)
|
||||
|
||||
Acauã is defensive software, but it processes hostile input: packet captures,
|
||||
Zeek logs and flow records may be malformed on purpose. Treat every dataset as
|
||||
untrusted.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Do not open a public issue with exploit details. Open a minimal issue asking for
|
||||
a private channel, or contact the maintainer through the Gitea profile listed on
|
||||
the repository. Include the affected version, the input shape and a minimal
|
||||
reproduction when a private channel is available.
|
||||
|
||||
## Scope and threat model
|
||||
|
||||
- **Input parsing:** a malformed log line must not panic the daemon, exhaust
|
||||
memory or escape the configured data directory.
|
||||
- **Path handling:** dataset paths are resolved and confined to the configured
|
||||
root; symbolic links are not followed outside it.
|
||||
- **API exposure:** `serve` binds to `127.0.0.1` by default. Exposing it on a
|
||||
network interface is an explicit operator decision and needs a reverse proxy
|
||||
with authentication.
|
||||
- **Evidence integrity:** alert evidence must be reproducible from the same
|
||||
input and code version. Silent nondeterminism is a correctness and security
|
||||
issue.
|
||||
- **Updates:** the CLI checks Gitea Releases, downloads over HTTPS and verifies
|
||||
SHA-256 before replacing the binary. The desktop app does not update itself.
|
||||
- **Privacy:** there is no telemetry. Retention defaults to seven days of
|
||||
normalized events; the operator is responsible for local law and policy.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Vulnerabilities in third-party Zeek, Suricata, Tauri or operating-system
|
||||
packages that are already fixed upstream.
|
||||
- Attacks that require the operator to disable the documented security controls
|
||||
(for example, running `serve` as root on a public interface).
|
||||
- Detection quality claims: the pillars are not implemented in this skeleton.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Only the latest `0.1.x` line is supported while the project is pre-alpha. There
|
||||
is no long-term support branch yet.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Política de segurança
|
||||
|
||||
[English](SECURITY.md) · **Português**
|
||||
|
||||
O Acauã é software defensivo, mas processa entrada hostil: capturas de pacotes,
|
||||
logs Zeek e registros de fluxo podem estar malformados de propósito. Trate todo
|
||||
dataset como não confiável.
|
||||
|
||||
## Reportando uma vulnerabilidade
|
||||
|
||||
Não abra issue pública com detalhes de exploração. Abra uma issue mínima pedindo
|
||||
um canal privado, ou contate o mantenedor pelo perfil no Gitea indicado no
|
||||
repositório. Inclua a versão afetada, o formato da entrada e uma reprodução
|
||||
mínima quando houver canal privado.
|
||||
|
||||
## Escopo e modelo de ameaça
|
||||
|
||||
- **Parsing de entrada:** uma linha malformada não pode derrubar o daemon,
|
||||
esgotar memória ou escapar do diretório de dados configurado.
|
||||
- **Caminhos:** caminhos de dataset são resolvidos e confinados à raiz
|
||||
configurada; links simbólicos não são seguidos para fora dela.
|
||||
- **Exposição da API:** o `serve` escuta em `127.0.0.1` por padrão. Expor em uma
|
||||
interface de rede é decisão explícita do operador e exige proxy reverso com
|
||||
autenticação.
|
||||
- **Integridade da evidência:** a evidência de um alerta precisa ser reproduzível
|
||||
com a mesma entrada e a mesma versão do código. Não determinismo silencioso é
|
||||
problema de correção e de segurança.
|
||||
- **Atualizações:** a CLI consulta Gitea Releases, baixa por HTTPS e verifica
|
||||
SHA-256 antes de substituir o binário. O app desktop não se atualiza sozinho.
|
||||
- **Privacidade:** não há telemetria. A retenção padrão é de sete dias de eventos
|
||||
normalizados; o operador responde pela lei e pela política local.
|
||||
|
||||
## Fora de escopo
|
||||
|
||||
- Vulnerabilidades em pacotes de terceiros (Zeek, Suricata, Tauri ou sistema
|
||||
operacional) já corrigidas upstream.
|
||||
- Ataques que exigem desligar controles documentados (por exemplo, rodar `serve`
|
||||
como root em interface pública).
|
||||
- Alegações de qualidade de detecção: os pilares não estão implementados neste
|
||||
esqueleto.
|
||||
|
||||
## Versões suportadas
|
||||
|
||||
Só a linha `0.1.x` mais recente é suportada enquanto o projeto é pré-alfa. Ainda
|
||||
não existe branch de suporte de longo prazo.
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "acaua-gui"
|
||||
description = "Acauã: graphical network threat hunting interface"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Acauã contributors"]
|
||||
repository = "https://git.lucasrguerra.dev.br/ciberfauna/acaua"
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
acaua-core = { path = "../../../crates/acaua-core" }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=../../../logo.png");
|
||||
let _ = std::fs::copy("../../../logo.png", "../ui/logo.png");
|
||||
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"]
|
||||
}
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 452 KiB |
@@ -0,0 +1,62 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use acaua_core::{doctor, update, zeek};
|
||||
|
||||
#[tauri::command]
|
||||
fn app_info() -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"mode": "desktop",
|
||||
"status": "skeleton"
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn doctor_report() -> Result<serde_json::Value, String> {
|
||||
serde_json::to_value(doctor::run()).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn analyze_dataset(path: String) -> Result<serde_json::Value, String> {
|
||||
let report = zeek::analyze_dir(&path).map_err(|error| error.to_string())?;
|
||||
serde_json::to_value(report).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_alerts() -> serde_json::Value {
|
||||
serde_json::json!([])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_check() -> Result<serde_json::Value, String> {
|
||||
let status = update::check(env!("CARGO_PKG_VERSION")).map_err(|error| error.to_string())?;
|
||||
serde_json::to_value(status).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_apply(app: tauri::AppHandle) -> Result<String, String> {
|
||||
use tauri::Emitter;
|
||||
let exe = std::env::var_os("APPIMAGE")
|
||||
.map(std::path::PathBuf::from)
|
||||
.filter(|p| p.is_file())
|
||||
.or_else(|| std::env::current_exe().ok())
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("acaua-gui"));
|
||||
update::apply_desktop_with_progress(&exe, move |phase| {
|
||||
let _ = app.emit("update-progress", serde_json::json!({ "phase": phase }));
|
||||
})
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
app_info,
|
||||
doctor_report,
|
||||
analyze_dataset,
|
||||
list_alerts,
|
||||
update_check,
|
||||
update_apply
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run Acauã");
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Acauã",
|
||||
"version": "0.1.0",
|
||||
"identifier": "dev.acaua.app",
|
||||
"build": {
|
||||
"frontendDist": "../ui"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "Acauã",
|
||||
"width": 1280,
|
||||
"height": 820,
|
||||
"minWidth": 460,
|
||||
"minHeight": 600,
|
||||
"resizable": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; img-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost; style-src 'self' 'unsafe-inline'; script-src 'self'; object-src 'none'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"category": "Utility",
|
||||
"shortDescription": "Caça a ameaças em rede",
|
||||
"longDescription": "Acauã é uma ferramenta de caça a ameaças em rede com evidência determinística, processamento local e sem IA opaca.",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #071018;
|
||||
--bg-2: #0b1722;
|
||||
--panel: rgba(15, 30, 43, 0.78);
|
||||
--panel-solid: #0f1e2b;
|
||||
--line: rgba(148, 188, 205, 0.18);
|
||||
--line-strong: rgba(148, 188, 205, 0.34);
|
||||
--text: #e8f2f4;
|
||||
--muted: #8fa8b3;
|
||||
--accent: #45d6b0;
|
||||
--accent-2: #f0b35b;
|
||||
--danger: #f27878;
|
||||
--shadow: 0 24px 60px rgba(0, 0, 0, 0.32);
|
||||
--radius: 16px;
|
||||
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||
--sans: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
html[data-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bg: #eef4f3;
|
||||
--bg-2: #e2ece9;
|
||||
--panel: rgba(255, 255, 255, 0.84);
|
||||
--panel-solid: #ffffff;
|
||||
--line: rgba(20, 58, 64, 0.14);
|
||||
--line-strong: rgba(20, 58, 64, 0.28);
|
||||
--text: #10262b;
|
||||
--muted: #5d7479;
|
||||
--accent: #087f68;
|
||||
--accent-2: #a05b09;
|
||||
--danger: #b42323;
|
||||
--shadow: 0 20px 50px rgba(20, 58, 64, 0.12);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
html, body { min-height: 100%; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--sans);
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(900px 520px at 12% -8%, rgba(69, 214, 176, 0.15), transparent 60%),
|
||||
radial-gradient(760px 480px at 95% 0%, rgba(240, 179, 91, 0.12), transparent 58%),
|
||||
linear-gradient(150deg, var(--bg), var(--bg-2));
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
button, input, select { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; }
|
||||
h1, h2, p { margin: 0; }
|
||||
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: 286px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 22px;
|
||||
padding: 28px 22px;
|
||||
border-right: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, rgba(8, 19, 28, 0.72), rgba(8, 19, 28, 0.36));
|
||||
backdrop-filter: blur(18px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
html[data-theme="light"] .sidebar {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.4));
|
||||
}
|
||||
|
||||
.brand { display: flex; align-items: center; gap: 12px; }
|
||||
.logo { width: 46px; height: 46px; object-fit: contain; filter: drop-shadow(0 8px 18px rgba(0, 0, 0, 0.25)); }
|
||||
.brand strong { display: block; font-size: 1.18rem; letter-spacing: 0.01em; }
|
||||
.brand small { display: block; color: var(--muted); font-size: 0.78rem; margin-top: 2px; }
|
||||
|
||||
.side-title {
|
||||
color: var(--muted);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav { display: grid; gap: 6px; }
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
color: var(--muted);
|
||||
transition: 160ms ease;
|
||||
}
|
||||
.nav-item:hover { border-color: var(--line); background: rgba(69, 214, 176, 0.06); color: var(--text); }
|
||||
.nav-item.active { border-color: var(--line-strong); background: rgba(69, 214, 176, 0.1); color: var(--text); }
|
||||
.nav-icon { width: 20px; color: var(--accent); text-align: center; }
|
||||
.nav-label { display: block; font-size: 0.92rem; font-weight: 600; }
|
||||
.nav-sub { display: block; font-size: 0.72rem; color: var(--muted); margin-top: 2px; }
|
||||
|
||||
.side-foot { margin-top: auto; display: grid; gap: 12px; }
|
||||
.status-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 5px 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.pill.muted { color: var(--muted); }
|
||||
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 12px var(--accent); }
|
||||
.pill.offline .dot { background: var(--danger); box-shadow: 0 0 12px var(--danger); }
|
||||
.mono { font-family: var(--mono); font-size: 0.75rem; color: var(--muted); }
|
||||
|
||||
.main { padding: 38px clamp(22px, 4vw, 56px) 42px; min-width: 0; }
|
||||
.topbar { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; margin-bottom: 26px; }
|
||||
.eyebrow { color: var(--accent); font-size: 0.72rem; letter-spacing: 0.2em; text-transform: uppercase; margin-bottom: 8px; }
|
||||
h1 { font-size: clamp(1.9rem, 3.3vw, 3.1rem); line-height: 1.04; letter-spacing: -0.035em; }
|
||||
.sub { color: var(--muted); margin-top: 10px; font-size: 0.98rem; }
|
||||
h2 { font-size: 1.08rem; letter-spacing: -0.01em; }
|
||||
|
||||
.icon-btn {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
color: var(--text);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
.icon-btn:hover { border-color: var(--line-strong); }
|
||||
|
||||
.notice {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel);
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.notice strong { color: var(--text); white-space: nowrap; }
|
||||
.notice-warn { border-color: rgba(240, 179, 91, 0.34); background: rgba(240, 179, 91, 0.07); }
|
||||
|
||||
.pillars {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
min-height: 178px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(160deg, var(--panel), rgba(15, 30, 43, 0.36));
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
html[data-theme="light"] .card { background: linear-gradient(160deg, var(--panel), rgba(255, 255, 255, 0.5)); }
|
||||
.card-kicker { color: var(--accent); font-size: 0.68rem; letter-spacing: 0.18em; text-transform: uppercase; }
|
||||
.card h2 { font-size: 1.02rem; }
|
||||
.card p { color: var(--muted); font-size: 0.83rem; line-height: 1.5; }
|
||||
.tag { margin-top: auto; align-self: flex-start; color: var(--accent-2); font-size: 0.68rem; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
|
||||
.panel {
|
||||
margin-top: 16px;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
scroll-margin-top: 20px;
|
||||
}
|
||||
.panel-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; margin-bottom: 18px; }
|
||||
.hint { color: var(--muted); font-size: 0.84rem; line-height: 1.5; margin-top: 5px; }
|
||||
|
||||
.field-row { display: flex; align-items: flex-end; gap: 10px; }
|
||||
.field { display: grid; gap: 7px; flex: 1; }
|
||||
.field > span { color: var(--muted); font-size: 0.74rem; letter-spacing: 0.08em; text-transform: uppercase; }
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
html[data-theme="light"] input, html[data-theme="light"] select { background: rgba(255, 255, 255, 0.7); }
|
||||
input:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(69, 214, 176, 0.12); }
|
||||
|
||||
.btn {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--text);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:hover { border-color: var(--accent); }
|
||||
.btn.primary { background: var(--accent); border-color: var(--accent); color: #06211b; }
|
||||
.btn.primary:hover { filter: brightness(1.06); }
|
||||
.btn:disabled { opacity: 0.5; cursor: progress; }
|
||||
|
||||
.link-btn { border: 0; background: transparent; color: var(--accent); font-size: 0.75rem; padding: 0; }
|
||||
.link-btn:hover { text-decoration: underline; }
|
||||
|
||||
.output {
|
||||
min-height: 92px;
|
||||
margin: 16px 0 0;
|
||||
padding: 14px;
|
||||
overflow: auto;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: var(--muted);
|
||||
font-family: var(--mono);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
html[data-theme="light"] .output { background: rgba(20, 58, 64, 0.04); }
|
||||
|
||||
.alert-list { display: grid; gap: 8px; margin: 0; padding: 0; list-style: none; }
|
||||
.alert-list li {
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
.alert-list .empty { color: var(--muted); border-style: dashed; }
|
||||
.alert-meta { display: flex; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
|
||||
.alert-meta strong { font-size: 0.78rem; color: var(--accent-2); }
|
||||
.alert-summary { color: var(--muted); line-height: 1.5; }
|
||||
|
||||
.foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 26px;
|
||||
color: var(--muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: min(460px, calc(100vw - 32px));
|
||||
padding: 22px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius);
|
||||
background: var(--panel-solid);
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
dialog::backdrop { background: rgba(2, 8, 12, 0.62); backdrop-filter: blur(3px); }
|
||||
.dialog-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
|
||||
dialog .field { margin-bottom: 14px; }
|
||||
dialog menu { display: flex; justify-content: flex-end; gap: 8px; margin: 20px 0 0; padding: 0; }
|
||||
.segmented { display: flex; gap: 4px; padding: 3px; border: 1px solid var(--line); border-radius: 10px; }
|
||||
.segmented button { flex: 1; padding: 7px 8px; border: 0; border-radius: 7px; background: transparent; color: var(--muted); font-size: 0.78rem; }
|
||||
.segmented button.active { background: rgba(69, 214, 176, 0.16); color: var(--text); }
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.pillars { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.app { grid-template-columns: 1fr; }
|
||||
.sidebar { position: static; height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.nav { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.side-foot { margin-top: 0; }
|
||||
.main { padding-top: 26px; }
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.pillars { grid-template-columns: 1fr; }
|
||||
.field-row { flex-direction: column; align-items: stretch; }
|
||||
.notice { flex-direction: column; gap: 4px; }
|
||||
.panel-head { flex-direction: column; }
|
||||
.foot { flex-direction: column; }
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import { setLang, tr } from './i18n.js';
|
||||
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const tauriInvoke = window.__TAURI__?.core?.invoke;
|
||||
|
||||
const connection = $('#connection');
|
||||
const connectionText = $('#connectionText');
|
||||
const version = $('#version');
|
||||
const updateBtn = $('#updateBtn');
|
||||
const datasetPath = $('#datasetPath');
|
||||
const analyzeBtn = $('#analyzeBtn');
|
||||
const refreshBtn = $('#refreshBtn');
|
||||
const output = $('#output');
|
||||
const alertList = $('#alertList');
|
||||
const settingsDlg = $('#settingsDlg');
|
||||
const settingsBtn = $('#settingsBtn');
|
||||
const closeSettings = $('#closeSettings');
|
||||
const doneSettings = $('#doneSettings');
|
||||
const langSel = $('#langSel');
|
||||
const themePick = $('#themePick');
|
||||
|
||||
let lastAlerts = [];
|
||||
let themePreference = localStorage.getItem('acaua-theme') || 'auto';
|
||||
|
||||
async function fetchJson(path, options = {}) {
|
||||
const response = await fetch(path, options);
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(body.error || response.statusText);
|
||||
return body;
|
||||
}
|
||||
|
||||
async function call(command, args = {}) {
|
||||
if (tauriInvoke) return tauriInvoke(command, args);
|
||||
switch (command) {
|
||||
case 'app_info':
|
||||
return fetchJson('/api/status');
|
||||
case 'analyze_dataset':
|
||||
return fetchJson('/api/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(args),
|
||||
});
|
||||
case 'list_alerts':
|
||||
return fetchJson('/api/alerts');
|
||||
case 'update_check':
|
||||
return fetchJson('/api/update');
|
||||
case 'doctor_report':
|
||||
return fetchJson('/api/health');
|
||||
default:
|
||||
throw new Error(`unsupported command: ${command}`);
|
||||
}
|
||||
}
|
||||
|
||||
function message(template, ...values) {
|
||||
let index = 0;
|
||||
return tr(template).replace(/\{n\}/g, () => values[index++] ?? '');
|
||||
}
|
||||
|
||||
function renderAlerts(alerts) {
|
||||
lastAlerts = Array.isArray(alerts) ? alerts : [];
|
||||
alertList.replaceChildren();
|
||||
if (!lastAlerts.length) {
|
||||
const empty = document.createElement('li');
|
||||
empty.className = 'empty';
|
||||
empty.textContent = tr('Nenhum alerta ainda.');
|
||||
alertList.append(empty);
|
||||
return;
|
||||
}
|
||||
for (const alert of lastAlerts) {
|
||||
const item = document.createElement('li');
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'alert-meta';
|
||||
const pillar = document.createElement('strong');
|
||||
pillar.textContent = message('Pilar {n}', alert.pillar);
|
||||
const score = document.createElement('span');
|
||||
score.className = 'mono';
|
||||
score.textContent = Number(alert.score).toFixed(3);
|
||||
meta.append(pillar, score);
|
||||
const summary = document.createElement('div');
|
||||
summary.className = 'alert-summary';
|
||||
summary.setAttribute('data-i18n-skip', '');
|
||||
summary.textContent = alert.summary;
|
||||
item.append(meta, summary);
|
||||
alertList.append(item);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
try {
|
||||
const info = await call('app_info');
|
||||
version.textContent = `v${info.version}`;
|
||||
connectionText.textContent = tr('Serviço local ativo');
|
||||
connection.classList.remove('offline');
|
||||
} catch (error) {
|
||||
connectionText.textContent = tr('Serviço indisponível');
|
||||
connection.classList.add('offline');
|
||||
version.textContent = '—';
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAlerts() {
|
||||
try {
|
||||
renderAlerts(await call('list_alerts'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeDataset() {
|
||||
const path = datasetPath.value.trim();
|
||||
if (!path) {
|
||||
output.textContent = tr('Nenhum dataset selecionado.');
|
||||
return;
|
||||
}
|
||||
analyzeBtn.disabled = true;
|
||||
output.textContent = tr('Analisando…');
|
||||
try {
|
||||
const report = await call('analyze_dataset', { path });
|
||||
output.textContent = message(
|
||||
'Análise concluída: {n} registros e {n} alertas.',
|
||||
report.records,
|
||||
report.alerts.length,
|
||||
);
|
||||
await refreshAlerts();
|
||||
} catch (error) {
|
||||
output.textContent = message('Falha na análise: {n}', error.message);
|
||||
} finally {
|
||||
analyzeBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUpdate() {
|
||||
updateBtn.disabled = true;
|
||||
updateBtn.textContent = tr('Verificando atualização…');
|
||||
try {
|
||||
const status = await call('update_check');
|
||||
updateBtn.textContent = status.update_available
|
||||
? message('Atualização {n} disponível.', status.latest_version)
|
||||
: message('Versão {n} é a mais recente.', status.current_version);
|
||||
} catch (error) {
|
||||
updateBtn.title = message('Não foi possível verificar a atualização: {n}', error.message);
|
||||
updateBtn.textContent = tr('checar atualização');
|
||||
} finally {
|
||||
updateBtn.disabled = false;
|
||||
window.setTimeout(() => {
|
||||
updateBtn.textContent = tr('checar atualização');
|
||||
updateBtn.title = '';
|
||||
}, 6000);
|
||||
}
|
||||
}
|
||||
|
||||
function applyTheme(preference) {
|
||||
themePreference = preference;
|
||||
localStorage.setItem('acaua-theme', preference);
|
||||
const light = window.matchMedia('(prefers-color-scheme: light)').matches;
|
||||
const effective = preference === 'auto' ? (light ? 'light' : 'dark') : preference;
|
||||
document.documentElement.dataset.theme = effective;
|
||||
themePick.querySelectorAll('[data-theme-pick]').forEach((button) => {
|
||||
button.classList.toggle('active', button.dataset.themePick === preference);
|
||||
});
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
langSel.value = localStorage.getItem('acaua-lang') || 'auto';
|
||||
settingsDlg.showModal();
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
settingsDlg.close();
|
||||
}
|
||||
|
||||
function setActiveNav(target) {
|
||||
document.querySelectorAll('.nav-item').forEach((button) => {
|
||||
button.classList.toggle('active', button.dataset.target === target);
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
applyTheme(themePreference);
|
||||
langSel.value = localStorage.getItem('acaua-lang') || 'auto';
|
||||
setLang(langSel.value);
|
||||
refreshStatus();
|
||||
refreshAlerts();
|
||||
|
||||
analyzeBtn.addEventListener('click', analyzeDataset);
|
||||
datasetPath.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') analyzeDataset();
|
||||
});
|
||||
refreshBtn.addEventListener('click', async () => {
|
||||
await refreshStatus();
|
||||
await refreshAlerts();
|
||||
});
|
||||
updateBtn.addEventListener('click', checkUpdate);
|
||||
settingsBtn.addEventListener('click', openSettings);
|
||||
closeSettings.addEventListener('click', closeDialog);
|
||||
doneSettings.addEventListener('click', closeDialog);
|
||||
langSel.addEventListener('change', () => {
|
||||
localStorage.setItem('acaua-lang', langSel.value);
|
||||
setLang(langSel.value);
|
||||
refreshStatus();
|
||||
renderAlerts(lastAlerts);
|
||||
updateBtn.textContent = tr('checar atualização');
|
||||
});
|
||||
themePick.addEventListener('click', (event) => {
|
||||
const button = event.target.closest('[data-theme-pick]');
|
||||
if (button) applyTheme(button.dataset.themePick);
|
||||
});
|
||||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
|
||||
if (themePreference === 'auto') applyTheme('auto');
|
||||
});
|
||||
document.querySelectorAll('.nav-item').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const target = button.dataset.target;
|
||||
setActiveNav(target);
|
||||
if (target === 'settings') openSettings();
|
||||
else document.getElementById(target)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -0,0 +1,66 @@
|
||||
export const EN = {
|
||||
'Acauã': 'Acauã',
|
||||
'caça a ameaças em rede': 'network threat hunting',
|
||||
'Navegação': 'Navigation',
|
||||
'Visão geral': 'Overview',
|
||||
'painel e estado': 'dashboard and status',
|
||||
'Alertas': 'Alerts',
|
||||
'evidências recentes': 'recent evidence',
|
||||
'Análise': 'Analysis',
|
||||
'dataset Zeek': 'Zeek dataset',
|
||||
'Configurações': 'Settings',
|
||||
'idioma e tema': 'language and theme',
|
||||
'Conectando…': 'Connecting…',
|
||||
'esqueleto': 'skeleton',
|
||||
'checar atualização': 'check for updates',
|
||||
'Caça a ameaças em rede': 'Network threat hunting',
|
||||
'Pesquisa determinística, sem IA opaca': 'Deterministic research, no opaque AI',
|
||||
'Aviso de escopo.': 'Scope notice.',
|
||||
'Este é um esqueleto pré-implementação. Os quatro pilares matemáticos, o parser Zeek e o armazenamento DuckDB ainda não estão implementados.': 'This is a pre-implementation skeleton. The four mathematical pillars, the Zeek parser and the DuckDB storage are not implemented yet.',
|
||||
'Uso autorizado.': 'Authorized use.',
|
||||
'Use somente em redes próprias ou com autorização explícita. Nenhum pacote é enviado para fora.': 'Use only on your own networks or with explicit authorization. No packet leaves the machine.',
|
||||
'Pilares de detecção': 'Detection pillars',
|
||||
'Pilar {n}': 'Pillar {n}',
|
||||
'Pilar 1': 'Pillar 1',
|
||||
'Periodograma de Rayleigh/Schuster. Em desenvolvimento.': 'Rayleigh/Schuster periodogram. Under development.',
|
||||
'em desenvolvimento': 'under development',
|
||||
'Pilar 2': 'Pillar 2',
|
||||
'Sessões persistentes': 'Persistent sessions',
|
||||
'Detector de conexões longas. Em desenvolvimento.': 'Long-connection detector. Under development.',
|
||||
'Pilar 3': 'Pillar 3',
|
||||
'Anomalias de protocolo': 'Protocol anomalies',
|
||||
'Features determinísticas por protocolo. Em desenvolvimento.': 'Deterministic per-protocol features. Under development.',
|
||||
'Pilar 4': 'Pillar 4',
|
||||
'Fusão de evidências': 'Evidence fusion',
|
||||
'Jensen-Shannon e prova por alerta. Em desenvolvimento.': 'Jensen-Shannon and per-alert proof. Under development.',
|
||||
'Analisar dataset': 'Analyze dataset',
|
||||
'Aponte para um diretório de logs Zeek ou para um dataset local.': 'Point to a Zeek log directory or a local dataset.',
|
||||
'Diretório de logs': 'Log directory',
|
||||
'Analisar': 'Analyze',
|
||||
'Atualizar status': 'Refresh status',
|
||||
'Nenhuma análise executada.': 'No analysis has run yet.',
|
||||
'Alertas recentes': 'Recent alerts',
|
||||
'Cada alerta futuro carregará a evidência determinística que o sustenta.': 'Every future alert will carry the deterministic evidence behind it.',
|
||||
'Nenhum alerta ainda. O motor de detecção será ligado nas próximas specs.': 'No alerts yet. The detection engine will be enabled in upcoming specs.',
|
||||
'Acauã · esqueleto pré-implementação': 'Acauã · pre-implementation skeleton',
|
||||
'Fechar configurações': 'Close settings',
|
||||
'Fechar': 'Close',
|
||||
'Idioma': 'Language',
|
||||
'Tema': 'Theme',
|
||||
'Claro': 'Light',
|
||||
'Escuro': 'Dark',
|
||||
'Navegação principal': 'Main navigation',
|
||||
'Serviço local ativo': 'Local service online',
|
||||
'Serviço indisponível': 'Service unavailable',
|
||||
'Nenhum dataset selecionado.': 'No dataset selected.',
|
||||
'Analisando…': 'Analyzing…',
|
||||
'Análise concluída: {n} registros e {n} alertas.': 'Analysis complete: {n} records and {n} alerts.',
|
||||
'Falha na análise: {n}': 'Analysis failed: {n}',
|
||||
'Verificando atualização…': 'Checking for updates…',
|
||||
'Versão {n} é a mais recente.': 'Version {n} is the latest.',
|
||||
'Atualização {n} disponível.': 'Update {n} available.',
|
||||
'Não foi possível verificar a atualização: {n}': 'Could not check for updates: {n}',
|
||||
'Nenhum alerta ainda.': 'No alerts yet.',
|
||||
'Alertas carregados: {n}': 'Alerts loaded: {n}',
|
||||
'versão {n}': 'version {n}',
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
import { EN } from './i18n-en.js';
|
||||
|
||||
let lang = 'pt';
|
||||
export const isEn = () => lang === 'en';
|
||||
export const currentLang = () => lang;
|
||||
export const LOC = () => (lang === 'en' ? 'en-US' : 'pt-BR');
|
||||
|
||||
export function resolveLang(pref) {
|
||||
if (pref === 'pt' || pref === 'en') return pref;
|
||||
return (navigator.language || 'pt').toLowerCase().startsWith('pt') ? 'pt' : 'en';
|
||||
}
|
||||
|
||||
const ATTRS = ['placeholder', 'title', 'aria-label', 'alt', 'data-empty'];
|
||||
const SKIP = '.ssid, .bssid, .mono, pre, code, input, textarea, script, style, [data-i18n-skip]';
|
||||
|
||||
const exact = new Map(Object.entries(EN).filter(([key]) => !key.includes('{n}')));
|
||||
const patterns = Object.entries(EN)
|
||||
.filter(([key]) => key.includes('{n}'))
|
||||
.map(([key, value]) => {
|
||||
const parts = key.split('{n}').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
return { re: new RegExp('^' + parts.join('(.+?)') + '$', 's'), to: value };
|
||||
})
|
||||
.sort((a, b) => b.re.source.length - a.re.source.length);
|
||||
|
||||
const cache = new Map();
|
||||
export function tr(text) {
|
||||
if (!isEn()) return text;
|
||||
const lead = text.match(/^\s*/)[0];
|
||||
const trail = text.match(/\s*$/)[0];
|
||||
const core = text.trim();
|
||||
if (!core) return text;
|
||||
if (cache.has(core)) return lead + cache.get(core) + trail;
|
||||
let out = exact.get(core);
|
||||
if (out === undefined) {
|
||||
for (const pattern of patterns) {
|
||||
const match = core.match(pattern.re);
|
||||
if (match) {
|
||||
let index = 0;
|
||||
out = pattern.to.replace(/\{n\}/g, () => tr(match[++index] ?? ''));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (out === undefined) out = core;
|
||||
if (cache.size > 5000) cache.clear();
|
||||
cache.set(core, out);
|
||||
return lead + out + trail;
|
||||
}
|
||||
|
||||
const original = new WeakMap();
|
||||
let observer = null;
|
||||
|
||||
function translateNode(node) {
|
||||
if (node.nodeType === 3) {
|
||||
if (node.parentElement?.closest(SKIP)) return;
|
||||
const source = original.get(node) ?? node.nodeValue;
|
||||
const translated = tr(source);
|
||||
if (translated !== node.nodeValue) {
|
||||
if (!original.has(node)) original.set(node, source);
|
||||
node.nodeValue = translated;
|
||||
}
|
||||
} else if (node.nodeType === 1) {
|
||||
if (node.matches?.(SKIP) && node.tagName !== 'INPUT') return;
|
||||
for (const attribute of ATTRS) {
|
||||
if (node.hasAttribute?.(attribute)) {
|
||||
const key = `i18n-${attribute}`;
|
||||
const source = node[key] ?? node.getAttribute(attribute);
|
||||
const translated = tr(source);
|
||||
if (translated !== node.getAttribute(attribute)) {
|
||||
node[key] = source;
|
||||
node.setAttribute(attribute, translated);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const child of node.childNodes) translateNode(child);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreAll(root) {
|
||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
||||
let node;
|
||||
while ((node = walker.nextNode())) {
|
||||
if (original.has(node)) {
|
||||
node.nodeValue = original.get(node);
|
||||
original.delete(node);
|
||||
}
|
||||
}
|
||||
root.querySelectorAll('*').forEach((element) => {
|
||||
for (const attribute of ATTRS) {
|
||||
const key = `i18n-${attribute}`;
|
||||
if (element[key] !== undefined) {
|
||||
element.setAttribute(attribute, element[key]);
|
||||
delete element[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function start() {
|
||||
translateNode(document.body);
|
||||
document.title = tr(document.title);
|
||||
if (observer) return;
|
||||
observer = new MutationObserver((mutations) => {
|
||||
observer.disconnect();
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === 'childList') mutation.addedNodes.forEach(translateNode);
|
||||
else if (mutation.type === 'characterData') {
|
||||
original.delete(mutation.target);
|
||||
translateNode(mutation.target);
|
||||
} else if (mutation.type === 'attributes') {
|
||||
delete mutation.target[`i18n-${mutation.attributeName}`];
|
||||
translateNode(mutation.target);
|
||||
}
|
||||
}
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ATTRS,
|
||||
});
|
||||
});
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ATTRS,
|
||||
});
|
||||
}
|
||||
|
||||
function stop() {
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
restoreAll(document.body);
|
||||
}
|
||||
|
||||
export function setLang(pref) {
|
||||
const next = resolveLang(pref);
|
||||
const changed = next !== lang;
|
||||
lang = next;
|
||||
document.documentElement.lang = lang === 'en' ? 'en' : 'pt-BR';
|
||||
if (lang === 'en') start();
|
||||
else if (changed || observer) stop();
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Acauã</title>
|
||||
<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="/logo.png" alt="">
|
||||
<div>
|
||||
<strong>Acauã</strong>
|
||||
<small>caça a ameaças em rede</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-title">Navegação</div>
|
||||
<nav class="nav" aria-label="Navegação principal">
|
||||
<button type="button" class="nav-item active" data-target="overview">
|
||||
<span class="nav-icon">◈</span>
|
||||
<span><span class="nav-label">Visão geral</span><span class="nav-sub">painel e estado</span></span>
|
||||
</button>
|
||||
<button type="button" class="nav-item" data-target="alerts">
|
||||
<span class="nav-icon">◎</span>
|
||||
<span><span class="nav-label">Alertas</span><span class="nav-sub">evidências recentes</span></span>
|
||||
</button>
|
||||
<button type="button" class="nav-item" data-target="analysis">
|
||||
<span class="nav-icon">⌁</span>
|
||||
<span><span class="nav-label">Análise</span><span class="nav-sub">dataset Zeek</span></span>
|
||||
</button>
|
||||
<button type="button" class="nav-item" data-target="settings">
|
||||
<span class="nav-icon">⚙</span>
|
||||
<span><span class="nav-label">Configurações</span><span class="nav-sub">idioma e tema</span></span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="side-foot">
|
||||
<div class="status-row">
|
||||
<span id="connection" class="pill"><span class="dot"></span><span id="connectionText">Conectando…</span></span>
|
||||
<span class="pill muted">esqueleto</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span id="version" class="mono" data-i18n-skip>—</span>
|
||||
<button id="updateBtn" type="button" class="link-btn">checar atualização</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main">
|
||||
<header class="topbar" id="overview">
|
||||
<div>
|
||||
<p class="eyebrow">Acauã</p>
|
||||
<h1>Caça a ameaças em rede</h1>
|
||||
<p class="sub">Pesquisa determinística, sem IA opaca</p>
|
||||
</div>
|
||||
<button id="settingsBtn" type="button" class="icon-btn" aria-label="Configurações" title="Configurações">⚙</button>
|
||||
</header>
|
||||
|
||||
<section class="notice notice-warn">
|
||||
<strong>Aviso de escopo.</strong>
|
||||
<span>Este é um esqueleto pré-implementação. Os quatro pilares matemáticos, o parser Zeek e o armazenamento DuckDB ainda não estão implementados.</span>
|
||||
</section>
|
||||
|
||||
<section class="notice">
|
||||
<strong>Uso autorizado.</strong>
|
||||
<span>Use somente em redes próprias ou com autorização explícita. Nenhum pacote é enviado para fora.</span>
|
||||
</section>
|
||||
|
||||
<section class="pillars" aria-label="Pilares de detecção">
|
||||
<article class="card">
|
||||
<span class="card-kicker">Pilar 1</span>
|
||||
<h2>Beaconing</h2>
|
||||
<p>Periodograma de Rayleigh/Schuster. Em desenvolvimento.</p>
|
||||
<span class="tag">em desenvolvimento</span>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-kicker">Pilar 2</span>
|
||||
<h2>Sessões persistentes</h2>
|
||||
<p>Detector de conexões longas. Em desenvolvimento.</p>
|
||||
<span class="tag">em desenvolvimento</span>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-kicker">Pilar 3</span>
|
||||
<h2>Anomalias de protocolo</h2>
|
||||
<p>Features determinísticas por protocolo. Em desenvolvimento.</p>
|
||||
<span class="tag">em desenvolvimento</span>
|
||||
</article>
|
||||
<article class="card">
|
||||
<span class="card-kicker">Pilar 4</span>
|
||||
<h2>Fusão de evidências</h2>
|
||||
<p>Jensen-Shannon e prova por alerta. Em desenvolvimento.</p>
|
||||
<span class="tag">em desenvolvimento</span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="analysis">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Analisar dataset</h2>
|
||||
<p class="hint">Aponte para um diretório de logs Zeek ou para um dataset local.</p>
|
||||
</div>
|
||||
<button id="refreshBtn" type="button" class="btn">Atualizar status</button>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<label class="field">
|
||||
<span>Diretório de logs</span>
|
||||
<input id="datasetPath" type="text" placeholder="/var/log/zeek/current" autocomplete="off" spellcheck="false">
|
||||
</label>
|
||||
<button id="analyzeBtn" type="button" class="btn primary">Analisar</button>
|
||||
</div>
|
||||
<pre id="output" class="output">Nenhuma análise executada.</pre>
|
||||
</section>
|
||||
|
||||
<section class="panel" id="alerts">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Alertas recentes</h2>
|
||||
<p class="hint">Cada alerta futuro carregará a evidência determinística que o sustenta.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="alertList" class="alert-list">
|
||||
<li class="empty">Nenhum alerta ainda. O motor de detecção será ligado nas próximas specs.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer class="foot">
|
||||
<span>Acauã · esqueleto pré-implementação</span>
|
||||
<span class="mono">MIT OR Apache-2.0</span>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<dialog id="settingsDlg" aria-labelledby="settingsTitle">
|
||||
<form method="dialog">
|
||||
<div class="dialog-head">
|
||||
<h2 id="settingsTitle">Configurações</h2>
|
||||
<button type="button" id="closeSettings" class="icon-btn" aria-label="Fechar configurações" title="Fechar">×</button>
|
||||
</div>
|
||||
<label class="field">
|
||||
<span>Idioma</span>
|
||||
<select id="langSel">
|
||||
<option value="auto">Auto</option>
|
||||
<option value="pt">Português</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="field">
|
||||
<span>Tema</span>
|
||||
<div class="segmented" id="themePick">
|
||||
<button type="button" data-theme-pick="auto">Auto</button>
|
||||
<button type="button" data-theme-pick="light">Claro</button>
|
||||
<button type="button" data-theme-pick="dark">Escuro</button>
|
||||
</div>
|
||||
</div>
|
||||
<menu>
|
||||
<button type="button" id="doneSettings" class="btn primary">Fechar</button>
|
||||
</menu>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
# Vendor
|
||||
|
||||
Third-party browser assets live here and are copied into the application at
|
||||
build time. Do not add a frontend framework or a package manager dependency for
|
||||
the interface: the UI stays vanilla HTML, CSS and JavaScript, like Guará and
|
||||
Sabiá.
|
||||
|
||||
Document every vendored file with its project URL, version and license.
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "acaua-cli"
|
||||
description = "Acauã: network threat hunting daemon and CLI"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "acaua"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
acaua-core = { path = "../acaua-core" }
|
||||
axum = "0.8"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
mime_guess = "2"
|
||||
rust-embed = "8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "signal"] }
|
||||
tower-http = { version = "0.6", features = ["compression-gzip", "cors", "trace"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
@@ -0,0 +1,107 @@
|
||||
mod web;
|
||||
|
||||
use acaua_core::{doctor, update, zeek};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
name = "acaua",
|
||||
about = "Acauã — caça determinística a ameaças em rede",
|
||||
version
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
Serve {
|
||||
#[arg(long, default_value = "127.0.0.1:8787")]
|
||||
bind: String,
|
||||
#[arg(long, default_value = ".acaua")]
|
||||
data_dir: PathBuf,
|
||||
},
|
||||
Analyze {
|
||||
directory: PathBuf,
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
Doctor {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
Update {
|
||||
#[arg(long)]
|
||||
apply: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
if let Err(error) = run(cli).await {
|
||||
eprintln!("erro: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(cli: Cli) -> acaua_core::Result<()> {
|
||||
match cli.command {
|
||||
Command::Serve { bind, data_dir } => web::serve(&bind, data_dir).await,
|
||||
Command::Analyze { directory, json } => {
|
||||
let report = zeek::analyze_dir(&directory)?;
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
} else {
|
||||
println!("diretório: {}", report.directory);
|
||||
println!("logs lidos: {}", report.logs_seen);
|
||||
println!("registros: {}", report.records);
|
||||
println!("alertas: {}", report.alerts.len());
|
||||
println!("estado: {}", report.status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Command::Doctor { json } => {
|
||||
let report = doctor::run();
|
||||
if json {
|
||||
println!("{}", serde_json::to_string_pretty(&report)?);
|
||||
} else {
|
||||
for check in &report.checks {
|
||||
println!(
|
||||
"{:<10} {:<5} {}",
|
||||
check.name,
|
||||
if check.ok { "ok" } else { "falha" },
|
||||
check.detail
|
||||
);
|
||||
}
|
||||
println!("estado: {}", report.status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Command::Update { apply } => {
|
||||
if apply {
|
||||
let executable = std::env::current_exe()?;
|
||||
let tag = update::apply(&executable)?;
|
||||
println!("atualizado para {tag}");
|
||||
} else {
|
||||
let status = update::check(env!("CARGO_PKG_VERSION"))?;
|
||||
println!(
|
||||
"versão atual: {} | última: {}",
|
||||
status.current_version,
|
||||
status.latest_version.as_deref().unwrap_or("desconhecida")
|
||||
);
|
||||
println!(
|
||||
"atualização disponível: {}",
|
||||
if status.update_available {
|
||||
"sim"
|
||||
} else {
|
||||
"não"
|
||||
}
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
use acaua_core::fusion::Alert;
|
||||
use acaua_core::storage::{DatasetSummary, Storage};
|
||||
use acaua_core::update::UpdateStatus;
|
||||
use acaua_core::zeek::{self, AnalysisReport};
|
||||
use acaua_core::{update, Result};
|
||||
use axum::body::Body;
|
||||
use axum::extract::State;
|
||||
use axum::http::{header, StatusCode, Uri};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
use tower_http::cors::CorsLayer;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
const LOGO: &[u8] = include_bytes!("../../../logo.png");
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "../../apps/gui/ui/"]
|
||||
struct Assets;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
storage: Arc<Storage>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Health {
|
||||
status: &'static str,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Status {
|
||||
version: &'static str,
|
||||
mode: &'static str,
|
||||
dataset: DatasetSummary,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorBody {
|
||||
error: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AnalyzeRequest {
|
||||
path: String,
|
||||
}
|
||||
|
||||
async fn health() -> Json<Health> {
|
||||
Json(Health {
|
||||
status: "ok",
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
})
|
||||
}
|
||||
|
||||
async fn status(State(state): State<AppState>) -> Json<Status> {
|
||||
Json(Status {
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
mode: "serve",
|
||||
dataset: state
|
||||
.storage
|
||||
.summary()
|
||||
.unwrap_or_else(|_| DatasetSummary::empty()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn alerts() -> Json<Vec<Alert>> {
|
||||
Json(Vec::new())
|
||||
}
|
||||
|
||||
async fn analyze(
|
||||
Json(request): Json<AnalyzeRequest>,
|
||||
) -> std::result::Result<Json<AnalysisReport>, (StatusCode, Json<ErrorBody>)> {
|
||||
zeek::analyze_dir(&request.path).map(Json).map_err(|error| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorBody {
|
||||
error: error.to_string(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_check() -> std::result::Result<Json<UpdateStatus>, (StatusCode, Json<ErrorBody>)> {
|
||||
update::check(env!("CARGO_PKG_VERSION"))
|
||||
.map(Json)
|
||||
.map_err(|error| {
|
||||
(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
Json(ErrorBody {
|
||||
error: error.to_string(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_apply() -> (StatusCode, Json<ErrorBody>) {
|
||||
(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
Json(ErrorBody {
|
||||
error: "update --apply is only available through the acaua CLI".to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn logo() -> impl IntoResponse {
|
||||
([(header::CONTENT_TYPE, "image/png")], Body::from(LOGO))
|
||||
}
|
||||
|
||||
async fn asset(uri: Uri) -> Response {
|
||||
let path = uri.path().trim_start_matches('/');
|
||||
let path = if path.is_empty() { "index.html" } else { path };
|
||||
match Assets::get(path) {
|
||||
Some(file) => {
|
||||
let mime = mime_guess::from_path(path).first_or_octet_stream();
|
||||
Response::builder()
|
||||
.header(header::CONTENT_TYPE, mime.as_ref())
|
||||
.body(Body::from(file.data.into_owned()))
|
||||
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
|
||||
}
|
||||
None => (StatusCode::NOT_FOUND, "not found").into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
|
||||
pub async fn serve(bind: &str, data_dir: PathBuf) -> Result<()> {
|
||||
let _ = tracing_subscriber::fmt().try_init();
|
||||
std::fs::create_dir_all(&data_dir)?;
|
||||
let state = AppState {
|
||||
storage: Arc::new(Storage::open(&data_dir)),
|
||||
};
|
||||
let app = Router::new()
|
||||
.route("/api/health", get(health))
|
||||
.route("/api/status", get(status))
|
||||
.route("/api/alerts", get(alerts))
|
||||
.route("/api/analyze", post(analyze))
|
||||
.route("/api/update", get(update_check).post(update_apply))
|
||||
.route("/logo.png", get(logo))
|
||||
.fallback(asset)
|
||||
.with_state(state)
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(CorsLayer::new())
|
||||
.layer(TraceLayer::new_for_http());
|
||||
let listener = TcpListener::bind(bind).await?;
|
||||
println!("Acauã listening on http://{bind}");
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "acaua-core"
|
||||
description = "Acauã: deterministic network threat hunting core"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
duckdb = ["dep:duckdb"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
thiserror = "1"
|
||||
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||
duckdb = { version = "1", features = ["bundled"], optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -0,0 +1,61 @@
|
||||
use serde::Serialize;
|
||||
use std::process::Command;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Check {
|
||||
pub name: &'static str,
|
||||
pub ok: bool,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DoctorReport {
|
||||
pub checks: Vec<Check>,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
fn command_available(name: &str, arg: &str) -> Check {
|
||||
match Command::new(name).arg(arg).output() {
|
||||
Ok(output) if output.status.success() => Check {
|
||||
name: if name == "zeek" { "zeek" } else { "runtime" },
|
||||
ok: true,
|
||||
detail: format!("{name} is available"),
|
||||
},
|
||||
Ok(_) => Check {
|
||||
name: if name == "zeek" { "zeek" } else { "runtime" },
|
||||
ok: false,
|
||||
detail: format!("{name} returned an error"),
|
||||
},
|
||||
Err(error) => Check {
|
||||
name: if name == "zeek" { "zeek" } else { "runtime" },
|
||||
ok: false,
|
||||
detail: format!("{name} not found: {error}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> DoctorReport {
|
||||
let checks = vec![
|
||||
command_available("zeek", "--version"),
|
||||
command_available("curl", "--version"),
|
||||
command_available("tar", "--version"),
|
||||
Check {
|
||||
name: "duckdb",
|
||||
ok: cfg!(feature = "duckdb"),
|
||||
detail: if cfg!(feature = "duckdb") {
|
||||
"bundled DuckDB feature is enabled".to_string()
|
||||
} else {
|
||||
"bundled DuckDB feature is disabled in this build".to_string()
|
||||
},
|
||||
},
|
||||
Check {
|
||||
name: "research",
|
||||
ok: true,
|
||||
detail: "research workspace is isolated from the product".to_string(),
|
||||
},
|
||||
];
|
||||
DoctorReport {
|
||||
checks,
|
||||
status: "skeleton",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AcauaError {
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
#[error("command failed: {cmd}: {detail}")]
|
||||
CommandFailed { cmd: String, detail: String },
|
||||
|
||||
#[error("feature not implemented yet: {0}")]
|
||||
NotImplemented(&'static str),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
#[error("serialization error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, AcauaError>;
|
||||
@@ -0,0 +1,25 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Alert {
|
||||
pub id: String,
|
||||
pub pillar: u8,
|
||||
pub score: f64,
|
||||
pub summary: String,
|
||||
pub evidence: serde_json::Value,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FusionReport {
|
||||
pub alerts: Vec<Alert>,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
pub fn fuse(_scores: &[f64]) -> Result<FusionReport> {
|
||||
Ok(FusionReport {
|
||||
alerts: Vec::new(),
|
||||
status: "skeleton",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
pub mod doctor;
|
||||
pub mod error;
|
||||
pub mod fusion;
|
||||
pub mod pillar1;
|
||||
pub mod pillar2;
|
||||
pub mod pillar3;
|
||||
pub mod pillar4;
|
||||
pub mod storage;
|
||||
pub mod update;
|
||||
pub mod zeek;
|
||||
|
||||
pub use error::{AcauaError, Result};
|
||||
@@ -0,0 +1,24 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct BeaconingInput {
|
||||
pub timestamps: Vec<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct BeaconingScore {
|
||||
pub score: f64,
|
||||
pub observations: usize,
|
||||
pub method: &'static str,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
pub fn score(input: &BeaconingInput) -> Result<BeaconingScore> {
|
||||
Ok(BeaconingScore {
|
||||
score: 0.0,
|
||||
observations: input.timestamps.len(),
|
||||
method: "rayleigh-schuster-stub",
|
||||
status: "skeleton",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SessionInput {
|
||||
pub durations_secs: Vec<f64>,
|
||||
pub bytes: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct SessionScore {
|
||||
pub score: f64,
|
||||
pub observations: usize,
|
||||
pub long_connections: usize,
|
||||
pub bowley_sentinel: Option<f64>,
|
||||
pub method: &'static str,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
pub fn score(input: &SessionInput) -> Result<SessionScore> {
|
||||
Ok(SessionScore {
|
||||
score: 0.0,
|
||||
observations: input.durations_secs.len(),
|
||||
long_connections: input
|
||||
.durations_secs
|
||||
.iter()
|
||||
.filter(|duration| **duration >= 300.0)
|
||||
.count(),
|
||||
bowley_sentinel: None,
|
||||
method: "long-connection-stub",
|
||||
status: "skeleton",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ProtocolInput {
|
||||
pub events: usize,
|
||||
pub protocols: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ProtocolScore {
|
||||
pub score: f64,
|
||||
pub observations: usize,
|
||||
pub distinct_protocols: usize,
|
||||
pub method: &'static str,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
pub fn score(input: &ProtocolInput) -> Result<ProtocolScore> {
|
||||
let mut protocols = input.protocols.clone();
|
||||
protocols.sort();
|
||||
protocols.dedup();
|
||||
Ok(ProtocolScore {
|
||||
score: 0.0,
|
||||
observations: input.events,
|
||||
distinct_protocols: protocols.len(),
|
||||
method: "jensen-shannon-stub",
|
||||
status: "skeleton",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct FusionInput {
|
||||
pub beaconing: f64,
|
||||
pub sessions: f64,
|
||||
pub protocol: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FusionScore {
|
||||
pub score: f64,
|
||||
pub inputs: usize,
|
||||
pub method: &'static str,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
pub fn score(input: &FusionInput) -> Result<FusionScore> {
|
||||
Ok(FusionScore {
|
||||
score: 0.0,
|
||||
inputs: [input.beaconing, input.sessions, input.protocol].len(),
|
||||
method: "deterministic-fusion-stub",
|
||||
status: "skeleton",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use crate::error::Result;
|
||||
use serde::Serialize;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DatasetSummary {
|
||||
pub records: u64,
|
||||
pub partitions: usize,
|
||||
pub ttl_days: u32,
|
||||
pub backend: &'static str,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
impl DatasetSummary {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
records: 0,
|
||||
partitions: 0,
|
||||
ttl_days: 7,
|
||||
backend: "duckdb-parquet",
|
||||
status: "skeleton",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Storage {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn open(root: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
root: root.as_ref().to_path_buf(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> Result<DatasetSummary> {
|
||||
Ok(DatasetSummary::empty())
|
||||
}
|
||||
|
||||
pub fn prune_expired(&self) -> Result<usize> {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
use crate::error::{AcauaError, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
const LATEST_RELEASE_URL: &str =
|
||||
"https://git.lucasrguerra.dev.br/api/v1/repos/ciberfauna/acaua/releases/latest";
|
||||
const RELEASES_PAGE_URL: &str = "https://git.lucasrguerra.dev.br/ciberfauna/acaua/releases";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Release {
|
||||
tag_name: String,
|
||||
assets: Vec<Asset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Asset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct UpdateStatus {
|
||||
pub current_version: String,
|
||||
pub latest_version: Option<String>,
|
||||
pub update_available: bool,
|
||||
}
|
||||
|
||||
fn curl_text(url: &str) -> Result<String> {
|
||||
let output = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: acaua-updater", url])
|
||||
.output()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !output.status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: String::from_utf8_lossy(&output.stderr).into_owned(),
|
||||
});
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn curl_download(url: &str, destination: &Path) -> Result<()> {
|
||||
let status = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: acaua-updater", "-o"])
|
||||
.arg(destination)
|
||||
.arg(url)
|
||||
.status()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: "download failed".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn asset_suffix_for_platform() -> String {
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else {
|
||||
"windows"
|
||||
};
|
||||
let arch = if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else {
|
||||
"aarch64"
|
||||
};
|
||||
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
|
||||
format!("-{os}-{arch}.{extension}")
|
||||
}
|
||||
|
||||
pub fn check(current_version: &str) -> Result<UpdateStatus> {
|
||||
let body = match curl_text(LATEST_RELEASE_URL) {
|
||||
Ok(body) => body,
|
||||
Err(_) => {
|
||||
return Ok(UpdateStatus {
|
||||
current_version: current_version.to_string(),
|
||||
latest_version: None,
|
||||
update_available: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
let latest = release.tag_name.trim_start_matches('v').to_string();
|
||||
let update_available = latest != current_version;
|
||||
Ok(UpdateStatus {
|
||||
current_version: current_version.to_string(),
|
||||
latest_version: Some(latest),
|
||||
update_available,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(current_exe: &Path) -> Result<String> {
|
||||
let is_cli = current_exe
|
||||
.file_stem()
|
||||
.map(|stem| stem == "acaua")
|
||||
.unwrap_or(false);
|
||||
if !is_cli {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"automatic updates only replace the acaua CLI; download the desktop installer from {RELEASES_PAGE_URL}"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
let suffix = asset_suffix_for_platform();
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.ends_with(&suffix))
|
||||
.ok_or_else(|| AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"no asset ending in `{suffix}` in release {}",
|
||||
release.tag_name
|
||||
),
|
||||
})?;
|
||||
let checksum_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|candidate| candidate.name == format!("{}.sha256", asset.name));
|
||||
|
||||
let temp_dir = std::env::temp_dir().join("acaua-update");
|
||||
fs::create_dir_all(&temp_dir)?;
|
||||
let archive_path = temp_dir.join(&asset.name);
|
||||
curl_download(&asset.browser_download_url, &archive_path)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
let expected_hash = expected
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let data = fs::read(&archive_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let actual_hash = format!("{:x}", hasher.finalize());
|
||||
if actual_hash != expected_hash {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "SHA-256 checksum mismatch; update discarded".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let extract_dir = temp_dir.join("extracted");
|
||||
let _ = fs::remove_dir_all(&extract_dir);
|
||||
fs::create_dir_all(&extract_dir)?;
|
||||
let status = Command::new("tar")
|
||||
.arg("xf")
|
||||
.arg(&archive_path)
|
||||
.arg("-C")
|
||||
.arg(&extract_dir)
|
||||
.status()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: "tar xf".to_string(),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "tar xf".to_string(),
|
||||
detail: "extraction failed".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let new_binary = find_binary(&extract_dir)?;
|
||||
let backup = current_exe.with_extension("bak");
|
||||
let _ = fs::remove_file(&backup);
|
||||
fs::rename(current_exe, &backup)?;
|
||||
fs::copy(&new_binary, current_exe)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = fs::metadata(current_exe)?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(current_exe, permissions)?;
|
||||
}
|
||||
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
fn find_binary(directory: &Path) -> Result<PathBuf> {
|
||||
fn is_binary(name: &OsStr) -> bool {
|
||||
name == "acaua" || name == "acaua.exe"
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
if is_binary(&entry.file_name()) {
|
||||
return Ok(entry.path());
|
||||
}
|
||||
if entry.file_type().map(|kind| kind.is_dir()).unwrap_or(false) {
|
||||
for inner in fs::read_dir(entry.path())? {
|
||||
let inner = inner?;
|
||||
if is_binary(&inner.file_name()) {
|
||||
return Ok(inner.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "binary not found in the downloaded package".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Desktop GUI installation type detected at runtime.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DesktopInstall {
|
||||
/// Linux AppImage: real filesystem path of the .AppImage file (from APPIMAGE env var).
|
||||
AppImage(PathBuf),
|
||||
/// Linux Debian package installed via dpkg: `current_exe` is reliable here.
|
||||
Deb,
|
||||
/// Windows NSIS installer: downloaded setup executable run with `/S`
|
||||
/// (silent). Does not relaunch the app on its own — see the comment on
|
||||
/// the `WindowsInstaller` apply path.
|
||||
WindowsInstaller,
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type given an optional `APPIMAGE` environment
|
||||
/// variable value and the current executable path.
|
||||
pub fn detect_desktop_install_from(
|
||||
appimage_env: Option<&std::ffi::OsStr>,
|
||||
current_exe: &Path,
|
||||
) -> Result<DesktopInstall> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Ok(DesktopInstall::WindowsInstaller)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(val) = appimage_env {
|
||||
let path = PathBuf::from(val);
|
||||
if path.is_file() {
|
||||
return Ok(DesktopInstall::AppImage(path));
|
||||
}
|
||||
}
|
||||
if current_exe.is_file()
|
||||
&& current_exe
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("appimage"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(DesktopInstall::AppImage(current_exe.to_path_buf()));
|
||||
}
|
||||
Ok(DesktopInstall::Deb)
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"automatic self-update is not supported on this platform; download manually from {RELEASES_PAGE_URL}"
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type using the live environment.
|
||||
pub fn detect_desktop_install() -> Result<DesktopInstall> {
|
||||
let exe = std::env::current_exe()?;
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), &exe)
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for a desktop installation and CPU architecture.
|
||||
pub fn gui_asset_suffix_for(install: &DesktopInstall, arch: &str) -> Result<&'static str> {
|
||||
match install {
|
||||
DesktopInstall::AppImage(_) => match arch {
|
||||
"x86_64" => Ok("_amd64.AppImage"),
|
||||
"aarch64" => Ok("_aarch64.AppImage"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for AppImage: {other}"),
|
||||
}),
|
||||
},
|
||||
DesktopInstall::Deb => match arch {
|
||||
"x86_64" => Ok("_amd64.deb"),
|
||||
"aarch64" => Ok("_arm64.deb"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for deb package: {other}"),
|
||||
}),
|
||||
},
|
||||
DesktopInstall::WindowsInstaller => match arch {
|
||||
"x86_64" => Ok("_x64-setup.exe"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for Windows installer: {other}"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for the current architecture.
|
||||
pub fn gui_asset_suffix(install: &DesktopInstall) -> Result<&'static str> {
|
||||
gui_asset_suffix_for(install, std::env::consts::ARCH)
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place.
|
||||
pub fn apply_desktop(current_exe: &Path) -> Result<String> {
|
||||
apply_desktop_with_progress(current_exe, |_| {})
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place,
|
||||
/// notifying the progress callback of phase transitions (`"downloading"`,
|
||||
/// `"verifying"`, `"installing"`, `"restarting"`).
|
||||
pub fn apply_desktop_with_progress<F>(current_exe: &Path, on_progress: F) -> Result<String>
|
||||
where
|
||||
F: Fn(&str),
|
||||
{
|
||||
let install =
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), current_exe)?;
|
||||
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
|
||||
let suffix = gui_asset_suffix(&install)?;
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.ends_with(suffix))
|
||||
.ok_or_else(|| AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"no asset ending with `{suffix}` in release {}",
|
||||
release.tag_name
|
||||
),
|
||||
})?;
|
||||
let checksum_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == format!("{}.sha256", asset.name));
|
||||
|
||||
let tmp_dir = std::env::temp_dir().join("acaua-desktop-update");
|
||||
fs::create_dir_all(&tmp_dir)?;
|
||||
let download_path = tmp_dir.join(&asset.name);
|
||||
|
||||
on_progress("downloading");
|
||||
curl_download(&asset.browser_download_url, &download_path)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
on_progress("verifying");
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
let expected_hash = expected
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let data = fs::read(&download_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let actual_hash: String = hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect();
|
||||
if actual_hash != expected_hash {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "SHA-256 checksum mismatch — update discarded".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
on_progress("installing");
|
||||
match install {
|
||||
DesktopInstall::AppImage(target_appimage) => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let parent_dir = target_appimage.parent().unwrap_or_else(|| Path::new("."));
|
||||
let temp_name = format!(
|
||||
".{}.tmp-{}",
|
||||
target_appimage
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("appimage"),
|
||||
std::process::id()
|
||||
);
|
||||
let temp_file = parent_dir.join(temp_name);
|
||||
fs::copy(&download_path, &temp_file)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&temp_file)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&temp_file, perms)?;
|
||||
}
|
||||
fs::rename(&temp_file, &target_appimage)?;
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(&target_appimage)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("failed to relaunch AppImage: {err}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&target_appimage, &download_path);
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "AppImage updates are only supported on Linux".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
DesktopInstall::Deb => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let status = Command::new("dpkg")
|
||||
.args(["-i"])
|
||||
.arg(&download_path)
|
||||
.status()
|
||||
.map_err(|e| AcauaError::CommandFailed {
|
||||
cmd: "dpkg -i".to_string(),
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "dpkg -i".to_string(),
|
||||
detail: format!("dpkg -i failed with exit code {:?}", status.code()),
|
||||
});
|
||||
}
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(current_exe)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("failed to relaunch updated app: {err}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&download_path, current_exe);
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "deb updates are only supported on Linux".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
DesktopInstall::WindowsInstaller => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// `/S` is the one NSIS silent-install switch with a documented,
|
||||
// stable meaning. There is no standard NSIS or Tauri flag that
|
||||
// makes a silent install relaunch the app afterwards — that was
|
||||
// claimed in an earlier draft of this code and could not be
|
||||
// verified against NSIS's or Tauri's documentation; passing an
|
||||
// invented flag to a stock NSIS installer is silently ignored,
|
||||
// not harmful, but also not a relaunch mechanism. Spawning
|
||||
// detached and exiting immediately (instead of waiting on the
|
||||
// child) also avoids a self-deadlock: if the installer's script
|
||||
// waits for this process to close before it can overwrite the
|
||||
// running .exe, blocking here on the installer's exit status
|
||||
// would hang forever. The practical effect: the update installs
|
||||
// silently, but the app does not reopen itself — the user needs
|
||||
// to start it again. This needs real-world confirmation on
|
||||
// Windows before relying on it; if testing shows the bundled
|
||||
// NSIS template does support an explicit relaunch flag or step,
|
||||
// wire it in here instead of this comment.
|
||||
on_progress("restarting");
|
||||
Command::new(&download_path)
|
||||
.arg("/S")
|
||||
.spawn()
|
||||
.map_err(|e| AcauaError::CommandFailed {
|
||||
cmd: "installer".to_string(),
|
||||
detail: format!("failed to launch installer: {e}"),
|
||||
})?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = &download_path;
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "Windows installer updates are only supported on Windows".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apply_refuses_to_overwrite_the_desktop_app() {
|
||||
let error = apply(Path::new("/opt/acaua/acaua-gui")).unwrap_err();
|
||||
assert!(error.to_string().contains("CLI"), "{error}");
|
||||
assert!(error.to_string().contains(RELEASES_PAGE_URL), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_desktop_install_variants() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let temp_appimage =
|
||||
std::env::temp_dir().join(format!("test-acaua-{}.AppImage", std::process::id()));
|
||||
let _ = fs::write(&temp_appimage, b"dummy appimage");
|
||||
|
||||
let res = detect_desktop_install_from(
|
||||
Some(temp_appimage.as_os_str()),
|
||||
Path::new("/usr/bin/acaua-gui"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::AppImage(temp_appimage.clone()));
|
||||
|
||||
let res_fallback = detect_desktop_install_from(None, &temp_appimage).unwrap();
|
||||
assert_eq!(
|
||||
res_fallback,
|
||||
DesktopInstall::AppImage(temp_appimage.clone())
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(&temp_appimage);
|
||||
|
||||
let res_deb =
|
||||
detect_desktop_install_from(None, Path::new("/usr/bin/acaua-gui")).unwrap();
|
||||
assert_eq!(res_deb, DesktopInstall::Deb);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let res = detect_desktop_install_from(
|
||||
None,
|
||||
Path::new("C:/Program Files/Acaua/acaua-gui.exe"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::WindowsInstaller);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_asset_suffix_matches_known_release_naming() {
|
||||
let appimage = DesktopInstall::AppImage(PathBuf::from("/tmp/Acaua.AppImage"));
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "x86_64").unwrap(),
|
||||
"_amd64.AppImage"
|
||||
);
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "aarch64").unwrap(),
|
||||
"_aarch64.AppImage"
|
||||
);
|
||||
|
||||
let deb = DesktopInstall::Deb;
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "x86_64").unwrap(), "_amd64.deb");
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "aarch64").unwrap(), "_arm64.deb");
|
||||
|
||||
let win = DesktopInstall::WindowsInstaller;
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&win, "x86_64").unwrap(),
|
||||
"_x64-setup.exe"
|
||||
);
|
||||
|
||||
assert!(gui_asset_suffix_for(&appimage, "mips").is_err());
|
||||
assert!(gui_asset_suffix_for(&deb, "riscv64").is_err());
|
||||
assert!(gui_asset_suffix_for(&win, "arm").is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::error::{AcauaError, Result};
|
||||
use crate::fusion::Alert;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ZeekLogKind {
|
||||
Conn,
|
||||
Dns,
|
||||
Ssl,
|
||||
SmbMapping,
|
||||
DceRpc,
|
||||
Rdp,
|
||||
Ssh,
|
||||
Kerberos,
|
||||
Ntlm,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AnalysisReport {
|
||||
pub directory: String,
|
||||
pub logs_seen: usize,
|
||||
pub records: u64,
|
||||
pub alerts: Vec<Alert>,
|
||||
pub status: &'static str,
|
||||
}
|
||||
|
||||
pub fn analyze_dir(directory: impl AsRef<Path>) -> Result<AnalysisReport> {
|
||||
let directory = directory.as_ref();
|
||||
if !directory.is_dir() {
|
||||
return Err(AcauaError::InvalidInput(format!(
|
||||
"{} is not a directory",
|
||||
directory.display()
|
||||
)));
|
||||
}
|
||||
Ok(AnalysisReport {
|
||||
directory: directory.display().to_string(),
|
||||
logs_seen: 0,
|
||||
records: 0,
|
||||
alerts: Vec::new(),
|
||||
status: "skeleton",
|
||||
})
|
||||
}
|
||||
|
||||
pub fn classify_log(name: &str) -> ZeekLogKind {
|
||||
match name {
|
||||
"conn.log" => ZeekLogKind::Conn,
|
||||
"dns.log" => ZeekLogKind::Dns,
|
||||
"ssl.log" => ZeekLogKind::Ssl,
|
||||
"smb_mapping.log" => ZeekLogKind::SmbMapping,
|
||||
"dce_rpc.log" => ZeekLogKind::DceRpc,
|
||||
"rdp.log" => ZeekLogKind::Rdp,
|
||||
"ssh.log" => ZeekLogKind::Ssh,
|
||||
"kerberos.log" => ZeekLogKind::Kerberos,
|
||||
"ntlm.log" => ZeekLogKind::Ntlm,
|
||||
_ => ZeekLogKind::Unknown,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
use acaua_core::{doctor, fusion, pillar1, pillar2, pillar3, pillar4, storage, zeek, AcauaError};
|
||||
|
||||
#[test]
|
||||
fn pillar1_returns_a_skeleton_score() {
|
||||
let input = pillar1::BeaconingInput {
|
||||
timestamps: vec![1.0, 2.0, 3.0],
|
||||
};
|
||||
let score = pillar1::score(&input).unwrap();
|
||||
assert_eq!(score.observations, 3);
|
||||
assert_eq!(score.status, "skeleton");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pillar2_counts_long_connections_without_panicking() {
|
||||
let input = pillar2::SessionInput {
|
||||
durations_secs: vec![1.0, 400.0],
|
||||
bytes: vec![10, 20],
|
||||
};
|
||||
let score = pillar2::score(&input).unwrap();
|
||||
assert_eq!(score.long_connections, 1);
|
||||
assert_eq!(score.status, "skeleton");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pillar3_handles_an_empty_protocol_list() {
|
||||
let score = pillar3::score(&pillar3::ProtocolInput::default()).unwrap();
|
||||
assert_eq!(score.distinct_protocols, 0);
|
||||
assert_eq!(score.status, "skeleton");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pillar4_fuses_three_inputs() {
|
||||
let score = pillar4::score(&pillar4::FusionInput::default()).unwrap();
|
||||
assert_eq!(score.inputs, 3);
|
||||
assert_eq!(score.status, "skeleton");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fusion_returns_no_alerts_yet() {
|
||||
let report = fusion::fuse(&[0.1, 0.2]).unwrap();
|
||||
assert!(report.alerts.is_empty());
|
||||
assert_eq!(report.status, "skeleton");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn storage_reports_the_reserved_backend() {
|
||||
let store = storage::Storage::open(std::env::temp_dir());
|
||||
let summary = store.summary().unwrap();
|
||||
assert_eq!(summary.ttl_days, 7);
|
||||
assert_eq!(summary.records, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeek_rejects_a_missing_directory() {
|
||||
let error = zeek::analyze_dir("/definitely/not/a/zeek/directory").unwrap_err();
|
||||
assert!(matches!(error, AcauaError::InvalidInput(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zeek_classifies_known_logs() {
|
||||
assert_eq!(zeek::classify_log("dns.log"), zeek::ZeekLogKind::Dns);
|
||||
assert_eq!(zeek::classify_log("other.log"), zeek::ZeekLogKind::Unknown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctor_reports_checks() {
|
||||
let report = doctor::run();
|
||||
assert!(!report.checks.is_empty());
|
||||
assert_eq!(report.status, "skeleton");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
services:
|
||||
zeek:
|
||||
image: zeek/zeek:latest
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_RAW
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
ZEEK_INTERFACE: ${ZEEK_INTERFACE:-eth0}
|
||||
command: ["zeek", "-i", "${ZEEK_INTERFACE:-eth0}", "Log::default_logdir=/logs"]
|
||||
volumes:
|
||||
- ./data/zeek:/logs
|
||||
restart: unless-stopped
|
||||
|
||||
acaua:
|
||||
build: .
|
||||
command: ["serve", "--bind", "0.0.0.0:8787", "--data-dir", "/data/acaua"]
|
||||
ports:
|
||||
- "127.0.0.1:8787:8787"
|
||||
volumes:
|
||||
- ./data/zeek:/data/zeek:ro
|
||||
- ./data/acaua:/data/acaua
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,177 @@
|
||||
# Architecture
|
||||
|
||||
**English** · [Português](pt-BR/arquitetura.md)
|
||||
|
||||
Acauã is a single Rust product: one daemon/CLI, one shared core, one embedded
|
||||
web UI and one Tauri shell. The shipped runtime has no Python and no external
|
||||
database. This document records the structural decisions and the six
|
||||
corrections applied to the original blueprint.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Choice | Why |
|
||||
|---|---|---|
|
||||
| Core, CLI and daemon | Rust 2021 | One toolchain, memory safety, no runtime dependency |
|
||||
| HTTP API | Axum + Tokio + Tower HTTP | Small async surface, easy to embed |
|
||||
| UI | Vanilla HTML/CSS/JS | No frontend build step, assets embedded in the binary |
|
||||
| Desktop | Tauri 2 | Native window over the same UI |
|
||||
| Analytical store | DuckDB + Parquet (optional `duckdb` feature) | Embedded, columnar, local-first |
|
||||
| Scientific validation | Python in `research/` only | astropy/scipy/networkx for fixtures, never shipped |
|
||||
|
||||
The workspace has two crates: `acaua-core` and `acaua-cli`. The GUI lives in
|
||||
`apps/gui`, **outside** the Cargo workspace, as in Guará and Sabiá. The spec
|
||||
mentioned `version.workspace = true` for the desktop manifest; keeping the GUI
|
||||
outside the workspace makes that impossible, so `check.sh` enforces SemVer
|
||||
equality across `Cargo.toml`, `acaua-cli/Cargo.toml`, the desktop manifest and
|
||||
`tauri.conf.json` instead. This is a deliberate deviation, not an oversight.
|
||||
|
||||
## Components
|
||||
|
||||
```
|
||||
Zeek logs ──► acaua-core::zeek ──► normalized events ──► storage
|
||||
│
|
||||
├──► pillar1 (beaconing)
|
||||
├──► pillar2 (persistent sessions)
|
||||
├──► pillar3 (protocol anomalies)
|
||||
└──► pillar4 (fusion) ──► alerts + evidence
|
||||
│
|
||||
acaua-cli ──► axum API ──► embedded UI ◄── Tauri shell ─────────────┘
|
||||
```
|
||||
|
||||
- `zeek`: bounded parser and path confinement. It must never follow a symlink
|
||||
outside the dataset root.
|
||||
- `storage`: DuckDB/Parquet writer with a seven-day TTL. Disabled by default so
|
||||
the skeleton does not require a C++ build.
|
||||
- `pillar1`–`pillar4`: pure functions over normalized events. They return
|
||||
scores plus evidence, never only a float.
|
||||
- `fusion`: fixed-order, bounded combination. It must be symmetric and
|
||||
reproducible.
|
||||
- `doctor`: local environment checks; no network calls except an explicit
|
||||
update check.
|
||||
- `update`: Gitea Releases client for the CLI only.
|
||||
|
||||
## The six corrections
|
||||
|
||||
### 1. Rayleigh/Schuster, not pure Lomb–Scargle
|
||||
|
||||
Event timestamps are a one-dimensional point process: there is no `y` value to
|
||||
fit. The primary periodogram is therefore Rayleigh/Schuster:
|
||||
|
||||
```text
|
||||
P(f) = |sum_k exp(2*pi*i*f*t_k)|^2 / N
|
||||
```
|
||||
|
||||
Generalized Lomb–Scargle is used only when a real `y` exists (for example bytes per bucket).
|
||||
This avoids inventing a dependent variable and keeps the null distribution
|
||||
honest.
|
||||
|
||||
### 2. Bowley skew with an explicit sentinel
|
||||
|
||||
The Bowley coefficient uses quartiles. When `Q3 == Q1`, the denominator is zero;
|
||||
the implementation returns a documented sentinel (`0.0` with an
|
||||
`degenerate_quartiles` evidence flag) instead of `NaN`, infinity or a panic. The
|
||||
sentinel is part of the alert record, not a silent fallback.
|
||||
|
||||
### 3. Jensen–Shannon, not raw KL
|
||||
|
||||
KL diverges when the reference distribution has a zero where the observed
|
||||
distribution does not. Fusion uses Jensen–Shannon, which is symmetric, bounded
|
||||
and defined for zero-probability bins with a small documented smoothing floor.
|
||||
This makes the score comparable across pillars and safe for sparse protocol
|
||||
histograms.
|
||||
|
||||
### 4. Persistent sessions get their own detector
|
||||
|
||||
A connection that stays open for hours never produces a short inter-arrival
|
||||
time, so a beaconing detector can miss it entirely. Pillar 2 scores duration,
|
||||
idle gaps and repeated long-lived flows directly. The two pillars may both fire;
|
||||
fusion treats that as separate evidence, not double counting of the same signal.
|
||||
|
||||
### 5. JA4 license caution
|
||||
|
||||
The client TLS JA4 fingerprint is usable. JA4S, JA4H, JA4X, JA4SSH and JA4L
|
||||
carry a non-OSI license and are **not** enabled by default. Any future use needs
|
||||
an explicit licensing decision and a documented opt-in. The default feature set
|
||||
must remain redistributable under MIT/Apache-2.0.
|
||||
|
||||
### 6. Determinism and per-alert JSON evidence
|
||||
|
||||
Every alert carries a JSON record: dataset hash, code version, pillar, inputs,
|
||||
intermediate values, sentinels, weights and final score. Reductions run in a
|
||||
fixed order; no hash-map iteration or thread scheduling may change a score.
|
||||
Given the same input and version, the evidence must be byte-identical.
|
||||
|
||||
## Data flow
|
||||
|
||||
1. `acaua analyze <dir>` validates the directory, reads supported logs and
|
||||
normalizes events. In this skeleton it validates and returns a stub report.
|
||||
2. Normalized events are written to the local store (when the `duckdb` feature
|
||||
is enabled) with a seven-day TTL.
|
||||
3. Each pillar runs over a deterministic window and returns candidate scores
|
||||
with evidence.
|
||||
4. Fusion combines candidates, applies thresholds and writes alerts.
|
||||
5. `serve` exposes the API and the embedded UI; the Tauri shell can open the same
|
||||
UI against a local or remote daemon.
|
||||
|
||||
## API
|
||||
|
||||
| Method | Path | Purpose |
|
||||
|---|---|---|
|
||||
| GET | `/api/health` | Liveness and version |
|
||||
| GET | `/api/status` | Uptime, data directory, pillar state |
|
||||
| GET | `/api/alerts` | Paginated alerts with evidence |
|
||||
| POST | `/api/analyze` | Analyze a dataset path |
|
||||
| POST | `/api/update` | Check for a CLI update |
|
||||
| GET | `/logo.png` | Embedded brand asset |
|
||||
| GET | `/*` | Embedded UI fallback |
|
||||
|
||||
`serve` binds to `127.0.0.1:8787` by default. There is no authentication layer in
|
||||
the skeleton; exposing it on a network interface requires an authenticating
|
||||
reverse proxy.
|
||||
|
||||
## Storage
|
||||
|
||||
DuckDB is an **optional** dependency (`features = ["duckdb"]`, `bundled`). The
|
||||
default skeleton does not compile the C++ backend. When enabled, the schema is
|
||||
append-only normalized events plus an alerts table, with Parquet export and a
|
||||
seven-day retention job. The database file lives under the configured data
|
||||
directory and never outside it.
|
||||
|
||||
## Updates
|
||||
|
||||
`update.rs` queries the Gitea Releases API for the `acaua` repository, selects
|
||||
the asset for the current platform, downloads over HTTPS with `curl`, verifies
|
||||
SHA-256 and replaces the CLI binary. It refuses to update a running GUI
|
||||
automatically; the desktop only reports that a new version exists. This mirrors
|
||||
the Sabiá updater and avoids a second update mechanism.
|
||||
|
||||
The Tauri updater plugin, its `latest.json` manifest, `updater.pubkey` and
|
||||
`createUpdaterArtifacts` are intentionally not used: they would add a second
|
||||
signing and distribution path, and the desktop must not self-update. The single
|
||||
CLI path is easier to audit and keeps release assets to tarballs plus SHA-256.
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
An earlier blueprint proposed a Python service with FastAPI and ClickHouse.
|
||||
Both are rejected. A Python service would add a second runtime, packaging burden
|
||||
and a network boundary inside a local-first tool; ClickHouse would add an
|
||||
external server and operational cost for datasets that fit an embedded columnar
|
||||
store. The product is 100% Rust, with Python confined to `research/`.
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Native:** `cargo build --release -p acaua-cli`, then `acaua serve`.
|
||||
- **Desktop:** Tauri bundles for Linux and Windows, built in CI.
|
||||
- **Docker:** a slim runtime image for lab use; see [docker.md](docker.md).
|
||||
|
||||
## Failure behavior
|
||||
|
||||
Malformed input is skipped with a counted, bounded diagnostic. A missing log is
|
||||
not fatal. A corrupt store is reported by `doctor` and never silently recreated
|
||||
over existing data. A failed update leaves the current binary in place.
|
||||
|
||||
## Versioning and compatibility
|
||||
|
||||
SemVer 2.0.0. Evidence JSON carries a schema version. Any change to a score or
|
||||
to the evidence shape is a breaking change for `0.x` minor versions and must be
|
||||
recorded in the changelog.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Terminal
|
||||
|
||||
**English** · [Português](pt-BR/cli.md)
|
||||
|
||||
The `acaua` binary is both the CLI and the daemon. This page describes the
|
||||
commands that exist in the `0.1.0` skeleton. Detection is not implemented yet;
|
||||
`analyze` validates input and returns a stub report.
|
||||
|
||||
```text
|
||||
acaua <command> [options]
|
||||
|
||||
Commands:
|
||||
serve Start the API and embedded UI
|
||||
analyze Analyze a Zeek log directory
|
||||
doctor Check the local environment
|
||||
update Check or apply a CLI update
|
||||
```
|
||||
|
||||
## `acaua serve`
|
||||
|
||||
Starts the HTTP API and the embedded UI.
|
||||
|
||||
```bash
|
||||
acaua serve --bind 127.0.0.1:8787 --data-dir .acaua
|
||||
```
|
||||
|
||||
| Option | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `--bind` | `127.0.0.1:8787` | Listen address |
|
||||
| `--data-dir` | `.acaua` | Store and runtime files |
|
||||
|
||||
The default bind is loopback-only. Exposing the daemon on a network requires a
|
||||
reverse proxy with authentication. The API is documented in
|
||||
[architecture.md](architecture.md#api).
|
||||
|
||||
## `acaua analyze`
|
||||
|
||||
```bash
|
||||
acaua analyze /var/log/zeek/current
|
||||
acaua analyze /var/log/zeek/current --json
|
||||
```
|
||||
|
||||
The directory is validated and a report is printed. In this skeleton the parser
|
||||
does not read Zeek records yet: `logs seen` and `records` are placeholders, and
|
||||
`alerts` is empty. The command is still useful to check path handling and the
|
||||
JSON shape.
|
||||
|
||||
The future parser will support `conn.log`, `dns.log`, `ssl.log` and the protocol
|
||||
logs needed for identity and lateral-movement features. NetFlow/IPFIX is a
|
||||
degraded mode; it is not equivalent to full protocol logs.
|
||||
|
||||
## `acaua doctor`
|
||||
|
||||
```bash
|
||||
acaua doctor
|
||||
acaua doctor --json
|
||||
```
|
||||
|
||||
Prints checks for Zeek, `curl`, `tar`, the optional DuckDB feature and the
|
||||
isolation of `research/`, followed by the status (`skeleton` in `0.1.0`). A
|
||||
missing optional dependency is reported as `falha` but does not abort the
|
||||
command. It does not send telemetry.
|
||||
|
||||
## `acaua update`
|
||||
|
||||
```bash
|
||||
acaua update
|
||||
acaua update --apply
|
||||
```
|
||||
|
||||
`update` queries the latest Gitea Release, compares the tag with the running
|
||||
version and prints whether an update exists. `--apply` downloads the archive for
|
||||
the current platform, verifies its SHA-256, extracts it and replaces the
|
||||
`acaua` binary.
|
||||
|
||||
Requirements: `curl` and `tar` on `PATH`, and a writable binary location. The
|
||||
updater refuses to replace any executable whose name is not `acaua`; the desktop
|
||||
app must be updated through its installer. A failed checksum leaves the current
|
||||
binary untouched.
|
||||
|
||||
## Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `0` | Success |
|
||||
| `1` | Runtime error (invalid path, failed update, I/O error) |
|
||||
| `2` | Invalid command-line usage (clap) |
|
||||
|
||||
## Environment
|
||||
|
||||
The skeleton has no required environment variable. `RUST_LOG` is reserved for
|
||||
the future tracing layer. The default data directory is relative to the current
|
||||
working directory; use an absolute path for a system service.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Local lab
|
||||
acaua doctor
|
||||
acaua analyze ./fixtures/zeek --json
|
||||
acaua serve --bind 127.0.0.1:8787 --data-dir ./.acaua
|
||||
|
||||
# Container
|
||||
docker run --rm -p 127.0.0.1:8787:8787 -v "$PWD/zeek:/data:ro" acaua serve
|
||||
```
|
||||
|
||||
See [docker.md](docker.md) for the image and volume layout.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Docker
|
||||
|
||||
**English** · [Português](pt-BR/docker.md)
|
||||
|
||||
The image is for lab and research use. It runs the same `acaua` binary as a
|
||||
native install, with the embedded UI. The skeleton does not enable the DuckDB
|
||||
feature in the image; the store remains reserved for a later release.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
docker build -t acaua .
|
||||
docker run --rm -p 127.0.0.1:8787:8787 -v "$PWD/data/acaua:/data/acaua" acaua
|
||||
```
|
||||
|
||||
The build stage uses `rust:1-slim-bookworm`; the runtime stage uses
|
||||
`debian:bookworm-slim` and installs only `ca-certificates`, `curl` and `tar`
|
||||
(the updater needs `curl` and `tar`).
|
||||
|
||||
## Compose
|
||||
|
||||
```bash
|
||||
cp .env.example .env # optional: set ZEEK_INTERFACE
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
The Compose file starts two services:
|
||||
|
||||
| Service | Purpose | Notes |
|
||||
|---|---|---|
|
||||
| `zeek` | Capture and write Zeek logs to `./data/zeek` | Host network and `NET_RAW`/`NET_ADMIN` |
|
||||
| `acaua` | Read logs and serve the UI/API | Host port bound to `127.0.0.1:8787` |
|
||||
|
||||
`acaua` mounts `./data/zeek` read-only at `/data/zeek` and `./data/acaua`
|
||||
read-write at `/data/acaua`. The daemon inside the container binds
|
||||
`0.0.0.0:8787`, but the published port is loopback-only on the host.
|
||||
|
||||
## Data layout
|
||||
|
||||
```text
|
||||
data/
|
||||
zeek/ # Zeek logs, written by the zeek service
|
||||
acaua/ # store, exports and runtime files
|
||||
```
|
||||
|
||||
Do not mount the host root or a directory containing unrelated sensitive files.
|
||||
The parser is expected to confine paths to the dataset root; the skeleton does
|
||||
not parse records yet.
|
||||
|
||||
## Security notes
|
||||
|
||||
- The `zeek` service needs packet capture privileges. Run it only on an
|
||||
interface and network you are authorized to monitor.
|
||||
- Do not change the host port mapping to `0.0.0.0:8787` without an
|
||||
authenticating reverse proxy.
|
||||
- The image has no telemetry and no outbound connection except an explicit
|
||||
`acaua update`.
|
||||
- The container is not a sandbox for hostile datasets. Treat a malformed capture
|
||||
as hostile input and keep the container unprivileged.
|
||||
|
||||
## Limitations
|
||||
|
||||
- No DuckDB build in the image; the analytical store is not active.
|
||||
- The UI is a shell and `analyze` returns a stub report.
|
||||
- The image does not include Zeek; the Compose file uses the upstream Zeek
|
||||
image.
|
||||
- There is no published image registry yet. Build locally.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Interface
|
||||
|
||||
**English** · [Português](pt-BR/interface.md)
|
||||
|
||||
The interface is a vanilla HTML/CSS/JS app embedded in the `acaua` binary and
|
||||
loaded by the Tauri shell. There is no frontend build step and no React/Next.js
|
||||
runtime. This page describes the `0.1.0` shell; it is not a screenshot tour,
|
||||
because the detection views have nothing real to show yet.
|
||||
|
||||
## Layout
|
||||
|
||||
- **Sidebar:** brand, navigation, connection state, version and update check.
|
||||
- **Overview:** scope notice, authorized-use notice and the four pillar cards.
|
||||
- **Analysis:** dataset path field, analyze button, refresh and raw JSON output.
|
||||
- **Alerts:** empty state explaining that the detection engine will arrive in a
|
||||
later spec.
|
||||
- **Settings:** language and theme, stored locally.
|
||||
|
||||
## Language
|
||||
|
||||
The source text is Portuguese. `i18n.js` applies English strings from
|
||||
`i18n-en.js` at runtime and persists the choice in `localStorage`. `Auto`
|
||||
follows the browser/system language. The check `node scripts/check-ui-i18n.mjs`
|
||||
fails when a visible Portuguese string has no English pair.
|
||||
|
||||
## Theme
|
||||
|
||||
`Auto`, light and dark are supported. `Auto` follows `prefers-color-scheme`. The
|
||||
choice is stored locally; no preference leaves the machine.
|
||||
|
||||
## Connection state
|
||||
|
||||
The footer polls `/api/health` and shows one of:
|
||||
|
||||
- **Connecting:** the first request has not finished.
|
||||
- **Local service online:** `/api/health` answered.
|
||||
- **Service unavailable:** the request failed; the shell remains usable for settings.
|
||||
|
||||
The Tauri shell can point at a local daemon or a remote `serve` in a future
|
||||
version. In `0.1.0` it uses the local API.
|
||||
|
||||
## Analysis view
|
||||
|
||||
`Analyze` posts the path to `/api/analyze` and prints the JSON response. The
|
||||
skeleton validates the path and returns a stub report. It does not parse Zeek
|
||||
records and does not render scores. Failures are shown as text, never as a
|
||||
silent empty state.
|
||||
|
||||
## Alerts view
|
||||
|
||||
The list is intentionally empty. A future alert row will show the pillar, score,
|
||||
timestamp and a link to the evidence record. The evidence must include inputs,
|
||||
intermediate values, sentinels, weights and the code version; a score alone is
|
||||
not enough.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Navigation uses real buttons and an `aria-label`.
|
||||
- The settings dialog uses `<dialog>` with a labelled title.
|
||||
- Status changes use text, not color alone.
|
||||
|
||||
## Limitations
|
||||
|
||||
- No real screenshots: the views are shells.
|
||||
- No alert detail, no charts and no dataset browser yet.
|
||||
- No authentication: the UI assumes a loopback daemon.
|
||||
- The desktop app does not self-update; it only reports an available version.
|
||||
@@ -0,0 +1,177 @@
|
||||
# Arquitetura
|
||||
|
||||
[English](../architecture.md) · **Português**
|
||||
|
||||
O Acauã é um único produto Rust: um daemon/CLI, um núcleo compartilhado, uma UI
|
||||
web embutida e um shell Tauri. O runtime distribuído não tem Python nem banco
|
||||
externo. Este documento registra as decisões estruturais e as seis correções
|
||||
aplicadas ao blueprint original.
|
||||
|
||||
## Stack
|
||||
|
||||
| Camada | Escolha | Por quê |
|
||||
|---|---|---|
|
||||
| Núcleo, CLI e daemon | Rust 2021 | Uma toolchain, segurança de memória, sem dependência de runtime |
|
||||
| API HTTP | Axum + Tokio + Tower HTTP | Superfície async pequena e fácil de embutir |
|
||||
| UI | HTML/CSS/JS vanilla | Sem etapa de build de frontend, assets embutidos no binário |
|
||||
| Desktop | Tauri 2 | Janela nativa sobre a mesma UI |
|
||||
| Armazenamento analítico | DuckDB + Parquet (feature opcional `duckdb`) | Embutido, colunar, local-first |
|
||||
| Validação científica | Python só em `research/` | astropy/scipy/networkx para fixtures, nunca distribuído |
|
||||
|
||||
O workspace tem dois crates: `acaua-core` e `acaua-cli`. A GUI fica em
|
||||
`apps/gui`, **fora** do workspace Cargo, como no Guará e no Sabiá. A spec citou
|
||||
`version.workspace = true` para o manifesto do desktop; manter a GUI fora do
|
||||
workspace torna isso impossível, então o `check.sh` impõe igualdade de SemVer
|
||||
entre `Cargo.toml`, `acaua-cli/Cargo.toml`, o manifesto do desktop e
|
||||
`tauri.conf.json`. É um desvio deliberado, não um esquecimento.
|
||||
|
||||
## Componentes
|
||||
|
||||
```
|
||||
Logs Zeek ──► acaua-core::zeek ──► eventos normalizados ──► armazenamento
|
||||
│
|
||||
├──► pilar1 (beaconing)
|
||||
├──► pilar2 (sessões persistentes)
|
||||
├──► pilar3 (anomalias de protocolo)
|
||||
└──► pilar4 (fusão) ──► alertas + evidência
|
||||
│
|
||||
acaua-cli ──► API axum ──► UI embutida ◄── shell Tauri ────────────┘
|
||||
```
|
||||
|
||||
- `zeek`: parser limitado e confinamento de caminho. Nunca segue link simbólico
|
||||
para fora da raiz do dataset.
|
||||
- `storage`: escritor DuckDB/Parquet com TTL de sete dias. Desligado por padrão
|
||||
para o esqueleto não exigir build C++.
|
||||
- `pillar1`–`pillar4`: funções puras sobre eventos normalizados. Devolvem
|
||||
pontuação e evidência, nunca só um float.
|
||||
- `fusion`: combinação limitada e em ordem fixa. Precisa ser simétrica e
|
||||
reproduzível.
|
||||
- `doctor`: checagens do ambiente local; sem chamadas de rede exceto a checagem
|
||||
explícita de atualização.
|
||||
- `update`: cliente de Gitea Releases só para a CLI.
|
||||
|
||||
## As seis correções
|
||||
|
||||
### 1. Rayleigh/Schuster, não Lomb–Scargle puro
|
||||
|
||||
Timestamps de eventos são um processo pontual unidimensional: não existe um `y`
|
||||
para ajustar. O periodograma primário é, portanto, Rayleigh/Schuster:
|
||||
|
||||
```text
|
||||
P(f) = |sum_k exp(2*pi*i*f*t_k)|^2 / N
|
||||
```
|
||||
|
||||
O Lomb–Scargle generalizado só é usado quando existe um `y` real (por exemplo,
|
||||
bytes por bucket). Isso evita inventar uma variável dependente e mantém a
|
||||
distribuição nula honesta.
|
||||
|
||||
### 2. Bowley com sentinela explícito
|
||||
|
||||
O coeficiente de Bowley usa quartis. Quando `Q3 == Q1`, o denominador é zero; a
|
||||
implementação devolve um sentinela documentado (`0.0` com a flag de evidência
|
||||
`degenerate_quartiles`) em vez de `NaN`, infinito ou panic. O sentinela faz parte
|
||||
do registro do alerta, não é fallback silencioso.
|
||||
|
||||
### 3. Jensen–Shannon, não KL puro
|
||||
|
||||
KL diverge quando a distribuição de referência tem zero onde a observada não
|
||||
tem. A fusão usa Jensen–Shannon, que é simétrica, limitada e definida para bins
|
||||
com probabilidade zero com um piso de suavização documentado. Isso torna a
|
||||
pontuação comparável entre pilares e segura para histogramas esparsos.
|
||||
|
||||
### 4. Sessões persistentes têm detector próprio
|
||||
|
||||
Uma conexão aberta por horas nunca produz um intervalo curto, então um detector
|
||||
de beaconing pode ignorá-la. O pilar 2 pontua duração, lacunas ociosas e fluxos
|
||||
longos repetidos diretamente. Os dois pilares podem disparar; a fusão trata isso
|
||||
como evidências separadas, não como contagem dupla do mesmo sinal.
|
||||
|
||||
### 5. Cautela com a licença JA4
|
||||
|
||||
A impressão digital JA4 de cliente TLS pode ser usada. JA4S, JA4H, JA4X, JA4SSH
|
||||
e JA4L têm licença não-OSI e **não** são habilitadas por padrão. Qualquer uso
|
||||
futuro exige decisão explícita de licenciamento e opt-in documentado. O conjunto
|
||||
padrão precisa continuar redistribuível sob MIT/Apache-2.0.
|
||||
|
||||
### 6. Determinismo e evidência JSON por alerta
|
||||
|
||||
Todo alerta carrega um registro JSON: hash do dataset, versão do código, pilar,
|
||||
entradas, valores intermediários, sentinelas, pesos e pontuação final. As
|
||||
reduções rodam em ordem fixa; iteração de hash map ou agendamento de threads não
|
||||
pode mudar uma pontuação. Com a mesma entrada e versão, a evidência precisa ser
|
||||
byte a byte idêntica.
|
||||
|
||||
## Fluxo de dados
|
||||
|
||||
1. `acaua analyze <dir>` valida o diretório, lê logs suportados e normaliza
|
||||
eventos. Neste esqueleto ele valida e devolve um relatório stub.
|
||||
2. Eventos normalizados vão para o armazenamento local (quando a feature
|
||||
`duckdb` está ligada) com TTL de sete dias.
|
||||
3. Cada pilar roda sobre uma janela determinística e devolve candidatos com
|
||||
evidência.
|
||||
4. A fusão combina candidatos, aplica limiares e grava alertas.
|
||||
5. `serve` expõe a API e a UI embutida; o shell Tauri abre a mesma UI contra um
|
||||
daemon local ou remoto.
|
||||
|
||||
## API
|
||||
|
||||
| Método | Caminho | Finalidade |
|
||||
|---|---|---|
|
||||
| GET | `/api/health` | Liveness e versão |
|
||||
| GET | `/api/status` | Uptime, diretório de dados, estado dos pilares |
|
||||
| GET | `/api/alerts` | Alertas paginados com evidência |
|
||||
| POST | `/api/analyze` | Analisar um caminho de dataset |
|
||||
| POST | `/api/update` | Checar atualização da CLI |
|
||||
| GET | `/logo.png` | Asset de marca embutido |
|
||||
| GET | `/*` | Fallback da UI embutida |
|
||||
|
||||
O `serve` escuta em `127.0.0.1:8787` por padrão. Não há camada de autenticação
|
||||
no esqueleto; expor em interface de rede exige proxy reverso com autenticação.
|
||||
|
||||
## Armazenamento
|
||||
|
||||
DuckDB é dependência **opcional** (`features = ["duckdb"]`, `bundled`). O
|
||||
esqueleto padrão não compila o backend C++. Quando ligado, o schema é de eventos
|
||||
normalizados append-only mais uma tabela de alertas, com exportação Parquet e
|
||||
retenção de sete dias. O arquivo do banco fica sob o diretório de dados
|
||||
configurado e nunca fora dele.
|
||||
|
||||
## Atualizações
|
||||
|
||||
`update.rs` consulta a API de Gitea Releases do repositório `acaua`, escolhe o
|
||||
asset da plataforma atual, baixa por HTTPS com `curl`, verifica SHA-256 e
|
||||
substitui o binário da CLI. Ele se recusa a atualizar automaticamente uma GUI em
|
||||
execução; o desktop só informa que existe versão nova. Isso espelha o updater do
|
||||
Sabiá e evita um segundo mecanismo de atualização.
|
||||
|
||||
O plugin updater do Tauri e seu fluxo de manifesto e assinatura não são usados
|
||||
de propósito: adicionariam um segundo caminho de assinatura e distribuição, e o
|
||||
desktop não deve se auto-atualizar. O caminho único da CLI é mais fácil de
|
||||
auditar e mantém os assets de release em tarballs mais SHA-256.
|
||||
|
||||
## Alternativas rejeitadas
|
||||
|
||||
Um blueprint anterior propunha um serviço Python e um banco colunar externo. Os
|
||||
dois são rejeitados. Um serviço Python adicionaria um segundo runtime, custo de
|
||||
empacotamento e uma fronteira de rede dentro de uma ferramenta local-first; um
|
||||
banco externo adicionaria servidor e custo operacional para datasets que cabem em
|
||||
um armazenamento colunar embutido. O produto é 100% Rust, com Python confinado a
|
||||
`research/`.
|
||||
|
||||
## Implantação
|
||||
|
||||
- **Nativa:** `cargo build --release -p acaua-cli`, depois `acaua serve`.
|
||||
- **Desktop:** bundles Tauri para Linux e Windows, construídos no CI.
|
||||
- **Docker:** imagem enxuta de runtime para laboratório; veja [docker.md](docker.md).
|
||||
|
||||
## Comportamento em falha
|
||||
|
||||
Entrada malformada é ignorada com diagnóstico contado e limitado. Log ausente não
|
||||
é fatal. Store corrompido é reportado pelo `doctor` e nunca recriado por cima de
|
||||
dados existentes. Atualização falha mantém o binário atual.
|
||||
|
||||
## Versionamento e compatibilidade
|
||||
|
||||
SemVer 2.0.0. O JSON de evidência carrega versão de schema. Qualquer mudança em
|
||||
pontuação ou no formato da evidência é quebra para versões menores `0.x` e
|
||||
precisa constar no changelog.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Terminal
|
||||
|
||||
[English](../cli.md) · **Português**
|
||||
|
||||
O binário `acaua` é ao mesmo tempo CLI e daemon. Esta página descreve os comandos
|
||||
que existem no esqueleto `0.1.0`. A detecção ainda não está implementada; o
|
||||
`analyze` valida a entrada e devolve um relatório stub.
|
||||
|
||||
```text
|
||||
acaua <comando> [opções]
|
||||
|
||||
Comandos:
|
||||
serve Sobe a API e a UI embutida
|
||||
analyze Analisa um diretório de logs Zeek
|
||||
doctor Checa o ambiente local
|
||||
update Checa ou aplica atualização da CLI
|
||||
```
|
||||
|
||||
## `acaua serve`
|
||||
|
||||
Sobe a API HTTP e a UI embutida.
|
||||
|
||||
```bash
|
||||
acaua serve --bind 127.0.0.1:8787 --data-dir .acaua
|
||||
```
|
||||
|
||||
| Opção | Padrão | Significado |
|
||||
|---|---|---|
|
||||
| `--bind` | `127.0.0.1:8787` | Endereço de escuta |
|
||||
| `--data-dir` | `.acaua` | Store e arquivos de runtime |
|
||||
|
||||
O bind padrão é só loopback. Expor o daemon na rede exige proxy reverso com
|
||||
autenticação. A API está documentada em
|
||||
[arquitetura.md](arquitetura.md#api).
|
||||
|
||||
## `acaua analyze`
|
||||
|
||||
```bash
|
||||
acaua analyze /var/log/zeek/current
|
||||
acaua analyze /var/log/zeek/current --json
|
||||
```
|
||||
|
||||
O diretório é validado e um relatório é impresso. Neste esqueleto o parser ainda
|
||||
não lê registros Zeek: `logs lidos` e `registros` são placeholders, e `alertas`
|
||||
vem vazio. O comando continua útil para conferir tratamento de caminho e o
|
||||
formato JSON.
|
||||
|
||||
O parser futuro vai suportar `conn.log`, `dns.log`, `ssl.log` e os logs de
|
||||
protocolo necessários para identidade e movimento lateral. NetFlow/IPFIX é modo
|
||||
degradado; não equivale a logs completos de protocolo.
|
||||
|
||||
## `acaua doctor`
|
||||
|
||||
```bash
|
||||
acaua doctor
|
||||
acaua doctor --json
|
||||
```
|
||||
|
||||
Imprime checagens de Zeek, `curl`, `tar`, da feature opcional DuckDB e do
|
||||
isolamento de `research/`, seguida do estado (`skeleton` em `0.1.0`). Dependência
|
||||
opcional ausente aparece como `falha`, mas não aborta o comando. Não envia
|
||||
telemetria.
|
||||
|
||||
## `acaua update`
|
||||
|
||||
```bash
|
||||
acaua update
|
||||
acaua update --apply
|
||||
```
|
||||
|
||||
O `update` consulta a última Release do Gitea, compara a tag com a versão em
|
||||
execução e informa se existe atualização. O `--apply` baixa o arquivo da
|
||||
plataforma atual, verifica o SHA-256, extrai e substitui o binário `acaua`.
|
||||
|
||||
Requisitos: `curl` e `tar` no `PATH`, e local de binário com permissão de escrita.
|
||||
O updater se recusa a substituir qualquer executável cujo nome não seja `acaua`;
|
||||
o app desktop deve ser atualizado pelo instalador. Checksum inválido mantém o
|
||||
binário atual intacto.
|
||||
|
||||
## Códigos de saída
|
||||
|
||||
| Código | Significado |
|
||||
|---|---|
|
||||
| `0` | Sucesso |
|
||||
| `1` | Erro de execução (caminho inválido, update falhou, erro de I/O) |
|
||||
| `2` | Uso inválido da linha de comando (clap) |
|
||||
|
||||
## Ambiente
|
||||
|
||||
O esqueleto não exige variável de ambiente. `RUST_LOG` está reservado para a
|
||||
camada de tracing futura. O diretório de dados padrão é relativo ao diretório de
|
||||
trabalho; use caminho absoluto em serviço de sistema.
|
||||
|
||||
## Exemplos
|
||||
|
||||
```bash
|
||||
# Laboratório local
|
||||
acaua doctor
|
||||
acaua analyze ./fixtures/zeek --json
|
||||
acaua serve --bind 127.0.0.1:8787 --data-dir ./.acaua
|
||||
|
||||
# Contêiner
|
||||
docker run --rm -p 127.0.0.1:8787:8787 -v "$PWD/zeek:/data:ro" acaua serve
|
||||
```
|
||||
|
||||
Veja [docker.md](docker.md) para a imagem e o layout de volumes.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Docker
|
||||
|
||||
[English](../docker.md) · **Português**
|
||||
|
||||
A imagem é para laboratório e pesquisa. Ela roda o mesmo binário `acaua` de uma
|
||||
instalação nativa, com a UI embutida. O esqueleto não liga a feature DuckDB na
|
||||
imagem; o armazenamento continua reservado para uma release futura.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
docker build -t acaua .
|
||||
docker run --rm -p 127.0.0.1:8787:8787 -v "$PWD/data/acaua:/data/acaua" acaua
|
||||
```
|
||||
|
||||
O estágio de build usa `rust:1-slim-bookworm`; o de runtime usa
|
||||
`debian:bookworm-slim` e instala só `ca-certificates`, `curl` e `tar` (o updater
|
||||
precisa de `curl` e `tar`).
|
||||
|
||||
## Compose
|
||||
|
||||
```bash
|
||||
cp .env.example .env # opcional: defina ZEEK_INTERFACE
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
O Compose sobe dois serviços:
|
||||
|
||||
| Serviço | Função | Notas |
|
||||
|---|---|---|
|
||||
| `zeek` | Captura e grava logs Zeek em `./data/zeek` | Rede do host e `NET_RAW`/`NET_ADMIN` |
|
||||
| `acaua` | Lê logs e serve UI/API | Porta do host presa a `127.0.0.1:8787` |
|
||||
|
||||
O `acaua` monta `./data/zeek` somente leitura em `/data/zeek` e `./data/acaua`
|
||||
leitura-escrita em `/data/acaua`. O daemon dentro do contêiner escuta
|
||||
`0.0.0.0:8787`, mas a porta publicada é só loopback no host.
|
||||
|
||||
## Layout de dados
|
||||
|
||||
```text
|
||||
data/
|
||||
zeek/ # logs Zeek, escritos pelo serviço zeek
|
||||
acaua/ # store, exportações e arquivos de runtime
|
||||
```
|
||||
|
||||
Não monte a raiz do host nem um diretório com arquivos sensíveis sem relação. O
|
||||
parser deve confinar caminhos à raiz do dataset; o esqueleto ainda não lê
|
||||
registros.
|
||||
|
||||
## Notas de segurança
|
||||
|
||||
- O serviço `zeek` precisa de privilégio de captura. Rode só em interface e rede
|
||||
que você tem autorização para monitorar.
|
||||
- Não mude o mapeamento de porta do host para `0.0.0.0:8787` sem proxy reverso
|
||||
com autenticação.
|
||||
- A imagem não tem telemetria e não faz conexão de saída exceto um
|
||||
`acaua update` explícito.
|
||||
- O contêiner não é sandbox para dataset hostil. Trate captura malformada como
|
||||
entrada hostil e mantenha o contêiner sem privilégios.
|
||||
|
||||
## Limitações
|
||||
|
||||
- Sem build DuckDB na imagem; o armazenamento analítico não está ativo.
|
||||
- A UI é um shell e o `analyze` devolve relatório stub.
|
||||
- A imagem não inclui Zeek; o Compose usa a imagem upstream do Zeek.
|
||||
- Ainda não há imagem publicada em registry. Compile localmente.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Interface
|
||||
|
||||
[English](../interface.md) · **Português**
|
||||
|
||||
A interface é um app HTML/CSS/JS vanilla embutido no binário `acaua` e carregado
|
||||
pelo shell Tauri. Não há etapa de build de frontend nem runtime React/Next.js.
|
||||
Esta página descreve o shell `0.1.0`; não é um tour de screenshots, porque as
|
||||
telas de detecção ainda não têm nada real para mostrar.
|
||||
|
||||
## Layout
|
||||
|
||||
- **Barra lateral:** marca, navegação, estado da conexão, versão e checagem de
|
||||
atualização.
|
||||
- **Visão geral:** aviso de escopo, aviso de uso autorizado e os quatro cards dos
|
||||
pilares.
|
||||
- **Análise:** campo de caminho do dataset, botão analisar, atualizar e saída
|
||||
JSON bruta.
|
||||
- **Alertas:** estado vazio explicando que o motor de detecção chega em spec
|
||||
futura.
|
||||
- **Configurações:** idioma e tema, guardados localmente.
|
||||
|
||||
## Idioma
|
||||
|
||||
O texto-fonte é em português. O `i18n.js` aplica as strings em inglês de
|
||||
`i18n-en.js` em tempo de execução e persiste a escolha no `localStorage`. `Auto`
|
||||
segue o idioma do navegador/sistema. A checagem
|
||||
`node scripts/check-ui-i18n.mjs` falha quando uma string visível em português não
|
||||
tem par em inglês.
|
||||
|
||||
## Tema
|
||||
|
||||
`Auto`, claro e escuro. `Auto` segue `prefers-color-scheme`. A escolha fica
|
||||
guardada localmente; nenhuma preferência sai da máquina.
|
||||
|
||||
## Estado da conexão
|
||||
|
||||
O rodapé consulta `/api/health` e mostra um de:
|
||||
|
||||
- **Conectando…:** a primeira requisição ainda não terminou.
|
||||
- **Serviço local ativo:** `/api/health` respondeu.
|
||||
- **Serviço indisponível:** a requisição falhou; o shell continua utilizável para
|
||||
configurações.
|
||||
|
||||
O shell Tauri poderá apontar para um daemon local ou um `serve` remoto em versão
|
||||
futura. Em `0.1.0` ele usa a API local.
|
||||
|
||||
## Tela de análise
|
||||
|
||||
`Analisar` envia o caminho para `/api/analyze` e imprime a resposta JSON. O
|
||||
esqueleto valida o caminho e devolve um relatório stub. Ele não lê registros Zeek
|
||||
e não renderiza pontuações. Falhas aparecem como texto, nunca como estado vazio
|
||||
silencioso.
|
||||
|
||||
## Tela de alertas
|
||||
|
||||
A lista está vazia de propósito. Uma linha futura de alerta vai mostrar pilar,
|
||||
pontuação, timestamp e link para o registro de evidência. A evidência precisa
|
||||
incluir entradas, valores intermediários, sentinelas, pesos e versão do código;
|
||||
pontuação sozinha não basta.
|
||||
|
||||
## Acessibilidade
|
||||
|
||||
- A navegação usa botões reais e `aria-label`.
|
||||
- O diálogo de configurações usa `<dialog>` com título rotulado.
|
||||
- Mudanças de estado usam texto, não só cor.
|
||||
|
||||
## Limitações
|
||||
|
||||
- Sem screenshots reais: as telas são shells.
|
||||
- Sem detalhe de alerta, gráficos ou navegador de dataset ainda.
|
||||
- Sem autenticação: a UI assume daemon em loopback.
|
||||
- O app desktop não se auto-atualiza; ele só informa a versão disponível.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Suporte
|
||||
|
||||
[English](../support.md) · **Português**
|
||||
|
||||
O Acauã é pré-alfa. Não há contrato de suporte nem binário publicado ainda. O
|
||||
caminho mais rápido para obter ajuda é uma issue objetiva no repositório Gitea.
|
||||
|
||||
## Antes de abrir uma issue
|
||||
|
||||
1. Rode `acaua doctor --json` e guarde a saída.
|
||||
2. Confirme a versão com `acaua --version`.
|
||||
3. Tente a menor entrada que reproduz o problema.
|
||||
4. Leia os [limites conhecidos](../../README.pt-BR.md#limites-conhecidos) e a
|
||||
[arquitetura](arquitetura.md).
|
||||
|
||||
## O que incluir
|
||||
|
||||
- Sistema operacional, arquitetura e versão do Acauã.
|
||||
- O comando exato e a saída completa do terminal.
|
||||
- Se a entrada é diretório de logs Zeek, fixture sintética ou captura.
|
||||
- Para problema de UI: build de navegador ou desktop, idioma e tema.
|
||||
- Para problema de atualização: saída de `acaua update` e se `curl` e `tar` estão
|
||||
no `PATH`.
|
||||
|
||||
Não anexe capturas de produção nem credenciais. Redija endereços, hostnames e
|
||||
payloads antes de compartilhar.
|
||||
|
||||
## Problemas de segurança
|
||||
|
||||
Não inclua detalhes de exploração em issue pública. Siga o
|
||||
[SECURITY.pt-BR.md](../../SECURITY.pt-BR.md) e peça um canal privado primeiro.
|
||||
|
||||
## Situações comuns
|
||||
|
||||
| Sintoma | O que checar |
|
||||
|---|---|
|
||||
| `analyze` devolve zero registros | Esperado em `0.1.0`; o parser é stub |
|
||||
| `serve` inacessível de outra máquina | Ele escuta em loopback por padrão |
|
||||
| `update` falha | `curl`, `tar`, permissão de escrita e assets da release |
|
||||
| Build do desktop falha | Dependências de sistema do Tauri para a plataforma |
|
||||
| UI aparece em português | `Auto` segue o idioma do sistema; escolha English nas configurações |
|
||||
|
||||
## Escopo
|
||||
|
||||
O suporte cobre o código e a documentação do Acauã. Não cobre configuração de
|
||||
Zeek ou Suricata, legalidade de captura de rede ou pacotes de distribuição de
|
||||
terceiros. Use somente em redes próprias ou com autorização para monitorar.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Support
|
||||
|
||||
**English** · [Português](pt-BR/suporte.md)
|
||||
|
||||
Acauã is pre-alpha. There is no support contract and no published binary yet.
|
||||
The fastest way to get help is a focused issue in the Gitea repository.
|
||||
|
||||
## Before opening an issue
|
||||
|
||||
1. Run `acaua doctor --json` and keep the output.
|
||||
2. Confirm the version with `acaua --version`.
|
||||
3. Try the smallest input that reproduces the problem.
|
||||
4. Read the [known limits](../README.md#known-limits) and
|
||||
[architecture](architecture.md).
|
||||
|
||||
## What to include
|
||||
|
||||
- Operating system, architecture and Acauã version.
|
||||
- The exact command and the full terminal output.
|
||||
- Whether the input is a Zeek log directory, a synthetic fixture or a capture.
|
||||
- For a UI problem: browser or desktop build, language and theme.
|
||||
- For an update problem: `acaua update` output and whether `curl` and `tar` are
|
||||
on `PATH`.
|
||||
|
||||
Do not attach production captures or credentials. Redact addresses, hostnames
|
||||
and payloads before sharing.
|
||||
|
||||
## Security issues
|
||||
|
||||
Do not include exploit details in a public issue. Follow
|
||||
[SECURITY.md](../SECURITY.md) and ask for a private channel first.
|
||||
|
||||
## Common situations
|
||||
|
||||
| Symptom | What to check |
|
||||
|---|---|
|
||||
| `analyze` returns zero records | Expected in `0.1.0`; the parser is a stub |
|
||||
| `serve` is unreachable from another machine | It binds to loopback by default |
|
||||
| `update` fails | `curl`, `tar`, write permission and release assets |
|
||||
| Desktop build fails | Tauri system dependencies for the platform |
|
||||
| UI shows Portuguese | `Auto` follows the system language; choose English in settings |
|
||||
|
||||
## Scope
|
||||
|
||||
Support covers the Acauã code and documentation. It does not cover Zeek or
|
||||
Suricata configuration, network capture legality, or third-party distribution
|
||||
packages. Use only on networks you own or are authorized to monitor.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Research workspace
|
||||
|
||||
**English** · [Português](README.pt-BR.md)
|
||||
|
||||
This directory is for scientific validation only. It is not part of the Acauã
|
||||
product, is not compiled by Cargo, and is never included in a release artifact.
|
||||
|
||||
Use it to generate reproducible fixtures, compare implementations against
|
||||
reference libraries, and run notebooks for the four detection pillars. The Rust
|
||||
engine must remain the only runtime used in production.
|
||||
|
||||
```bash
|
||||
cd research
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install -e .
|
||||
jupyter lab
|
||||
```
|
||||
|
||||
The Python environment is isolated from the workspace. Adding a dependency here
|
||||
does not add it to the shipped binary.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Espaço de pesquisa
|
||||
|
||||
[English](README.md) · **Português**
|
||||
|
||||
Este diretório serve só para validação científica. Ele não faz parte do produto
|
||||
Acauã, não é compilado pelo Cargo e nunca entra em um artefato de release.
|
||||
|
||||
Use-o para gerar fixtures reproduzíveis, comparar implementações com bibliotecas
|
||||
de referência e rodar notebooks dos quatro pilares de detecção. O motor Rust
|
||||
continua sendo o único runtime usado em produção.
|
||||
|
||||
```bash
|
||||
cd research
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install -e .
|
||||
jupyter lab
|
||||
```
|
||||
|
||||
O ambiente Python é isolado do workspace. Adicionar uma dependência aqui não a
|
||||
adiciona ao binário distribuído.
|
||||
@@ -0,0 +1,17 @@
|
||||
[project]
|
||||
name = "acaua-research"
|
||||
version = "0.1.0"
|
||||
description = "Scientific validation fixtures and notebooks for Acauã"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"astropy>=6.0",
|
||||
"scipy>=1.13",
|
||||
"networkx>=3.3",
|
||||
"pandas>=2.2",
|
||||
"matplotlib>=3.9",
|
||||
"jupyterlab>=4.2",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=70"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs apt packages in CI without depending on fast mirrors: reuses cached .deb
|
||||
# archives when present and retries with per-attempt timeouts if apt stalls or fails.
|
||||
#
|
||||
# Usage: scripts/apt-install.sh <cache-dir> <package>...
|
||||
set -euo pipefail
|
||||
[ $# -ge 2 ] || { echo "usage: $0 <cache-dir> <package>..." >&2; exit 2; }
|
||||
cache="$1"; shift
|
||||
pkgs=("$@")
|
||||
mkdir -p "$cache/partial"
|
||||
# apt runs as root and leaves files (lock, .deb) unreadable by non-root runner users;
|
||||
# restore ownership on exit so cache directories remain accessible.
|
||||
trap 'sudo chown -R "$(id -u):$(id -g)" "$cache" 2>/dev/null || true; rm -f "$cache/lock"' EXIT
|
||||
opts=(-o "Dir::Cache::archives=$cache" -y)
|
||||
|
||||
# 1) With cached .deb files and package index present: install directly without updating apt lists
|
||||
if [ -z "$(dpkg --print-foreign-architectures)" ] && ls "$cache"/*.deb >/dev/null 2>&1 && ls /var/lib/apt/lists/*Packages* >/dev/null 2>&1 && timeout 300 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
|
||||
echo "installed from cache"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2) Cache miss (or multiarch / incomplete): update and install, up to 3 attempts with timeout
|
||||
for attempt in 1 2 3; do
|
||||
if timeout 240 sudo apt-get update && { [ -z "$(dpkg --print-foreign-architectures)" ] || timeout 600 sudo apt-get "${opts[@]}" upgrade; } && timeout 600 sudo apt-get "${opts[@]}" install "${pkgs[@]}"; then
|
||||
echo "installed (attempt $attempt)"
|
||||
exit 0
|
||||
fi
|
||||
echo "apt failed or timed out (attempt $attempt of 3)" >&2
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
echo "failed to install: ${pkgs[*]}" >&2
|
||||
exit 1
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the desktop app (Tauri) and places the resulting bundles in dist-gui/.
|
||||
#
|
||||
# scripts/build-gui.sh linux # AppImage and .deb (native architecture)
|
||||
# scripts/build-gui.sh linux-aarch64 # AppImage and .deb for aarch64
|
||||
# scripts/build-gui.sh linux-x86_64 # AppImage and .deb for x86_64 (native or cross-built from aarch64)
|
||||
# scripts/build-gui.sh windows # NSIS installer, cross-built from Linux via cargo-xwin
|
||||
# scripts/build-gui.sh windows-docker # Same as above inside an isolated container
|
||||
#
|
||||
# Windows builds use cargo-xwin and NSIS (requires: nsis, lld, llvm, clang, and
|
||||
# `cargo install cargo-xwin`).
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
platform=${1:?usage: build-gui.sh <linux|linux-x86_64|linux-aarch64|windows|windows-docker>}
|
||||
tauri() { npx --yes @tauri-apps/cli@2 "$@"; }
|
||||
|
||||
|
||||
if [ "$platform" = windows-docker ]; then
|
||||
docker volume create acaua-cargo >/dev/null
|
||||
docker volume create acaua-rustup >/dev/null
|
||||
mkdir -p dist-gui
|
||||
exec docker run --rm -v "$PWD":/src -v acaua-cargo:/root/.cargo -v acaua-rustup:/root/.rustup \
|
||||
-w /src -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
|
||||
ubuntu:24.04 bash -c '
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl ca-certificates build-essential pkg-config nsis lld llvm clang nodejs npm unzip >/dev/null
|
||||
[ -x /root/.cargo/bin/rustup ] || curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal >/dev/null
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
|
||||
scripts/build-gui.sh windows
|
||||
chown -R "$HOST_UID:$HOST_GID" dist-gui apps/gui/src-tauri/target 2>/dev/null || true'
|
||||
fi
|
||||
|
||||
rm -rf dist-gui
|
||||
mkdir -p dist-gui
|
||||
cd apps/gui/src-tauri
|
||||
|
||||
# Generate derived icons if missing.
|
||||
[ -f icons/icon.ico ] || tauri icon icons/source.png -o icons
|
||||
|
||||
tauri_blob_cache="$HOME/.cache/tauri"
|
||||
if [ -d /opt/hostedtoolcache ] && [ -w /opt/hostedtoolcache ]; then
|
||||
tauri_blob_cache="/opt/hostedtoolcache/tauri-tools"
|
||||
fi
|
||||
mkdir -p "$tauri_blob_cache" "$HOME/.cache/tauri"
|
||||
|
||||
fetch_cached_tool() {
|
||||
local name=$1 url=$2 patch_elf=${3:-0}
|
||||
local cached="$tauri_blob_cache/$name"
|
||||
if [ ! -s "$cached" ]; then
|
||||
local tmp="$cached.tmp.$$"
|
||||
curl -fsSL "$url" -o "$tmp"
|
||||
if [ "$patch_elf" = 1 ]; then
|
||||
dd if=/dev/zero of="$tmp" bs=1 count=3 seek=8 conv=notrunc 2>/dev/null
|
||||
fi
|
||||
chmod +x "$tmp"
|
||||
mv -f "$tmp" "$cached"
|
||||
fi
|
||||
if [ "$cached" != "$HOME/.cache/tauri/$name" ]; then
|
||||
cp -f "$cached" "$HOME/.cache/tauri/$name"
|
||||
chmod +x "$HOME/.cache/tauri/$name"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$platform" in
|
||||
linux|linux-aarch64)
|
||||
bundle="${CARGO_TARGET_DIR:-target}/release/bundle"
|
||||
rm -rf "$bundle/appimage" "$bundle/deb"
|
||||
tauri build --bundles appimage,deb
|
||||
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
||||
;;
|
||||
linux-x86_64)
|
||||
if [ "$(uname -m)" = x86_64 ]; then
|
||||
bundle="${CARGO_TARGET_DIR:-target}/release/bundle"
|
||||
rm -rf "$bundle/appimage" "$bundle/deb"
|
||||
tauri build --bundles appimage,deb
|
||||
else
|
||||
rustup target add x86_64-unknown-linux-gnu >/dev/null
|
||||
mkdir -p /tmp/x86_64-cross-tools "$HOME/.cache/tauri"
|
||||
cat >/tmp/x86_64-cross-tools/ldd <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exec qemu-x86_64 /lib64/ld-linux-x86-64.so.2 --list "$@"
|
||||
EOF
|
||||
chmod +x /tmp/x86_64-cross-tools/ldd
|
||||
ln -sf /usr/bin/x86_64-linux-gnu-strip /tmp/x86_64-cross-tools/strip
|
||||
for tool_rel in glib-2.0/gio-querymodules gdk-pixbuf-2.0/gdk-pixbuf-query-loaders libgtk-3-0t64/gtk-query-immodules-3.0 libgtk-3-0/gtk-query-immodules-3.0; do
|
||||
if [ -x "/usr/lib/x86_64-linux-gnu/$tool_rel" ]; then
|
||||
tname=$(basename "$tool_rel")
|
||||
printf '#!/usr/bin/env bash\nexec qemu-x86_64 "/usr/lib/x86_64-linux-gnu/%s" "$@"\n' "$tool_rel" >"/tmp/x86_64-cross-tools/$tname"
|
||||
chmod +x "/tmp/x86_64-cross-tools/$tname"
|
||||
fi
|
||||
done
|
||||
fetch_cached_tool "linuxdeploy-07333c6-aarch64.AppImage" \
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy-07333c6/linuxdeploy-aarch64.AppImage" 1
|
||||
# tauri-bundler looks for this exact cache name (no arch suffix); the
|
||||
# architecture only appears in the download URL.
|
||||
fetch_cached_tool "linuxdeploy-plugin-appimage.AppImage" \
|
||||
"https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-aarch64.AppImage" 1
|
||||
fetch_cached_tool "AppRun-x86_64" \
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-x86_64" 0
|
||||
fetch_cached_tool "runtime-x86_64" \
|
||||
"https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" 0
|
||||
# tauri-bundler runs `dd ... seek=8` on this path to hide linuxdeploy from
|
||||
# AppImage integration tools. A shell-script wrapper loses its shebang and
|
||||
# then fails with ENOENT, so use a native ELF launcher: the dd writes into
|
||||
# ELF padding and the launcher stays executable.
|
||||
launcher_src=/tmp/x86_64-cross-tools/linuxdeploy-x86_64.c
|
||||
launcher_bin="$HOME/.cache/tauri/linuxdeploy-07333c6-x86_64.AppImage"
|
||||
cat >"$launcher_src" <<'EOF'
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *home = getenv("HOME");
|
||||
if (home == NULL) { fputs("HOME is not set\n", stderr); return 127; }
|
||||
char target[4096];
|
||||
char runtime[4096];
|
||||
char path[8192];
|
||||
const char *old_path = getenv("PATH");
|
||||
snprintf(target, sizeof target, "%s/.cache/tauri/linuxdeploy-07333c6-aarch64.AppImage", home);
|
||||
snprintf(runtime, sizeof runtime, "%s/.cache/tauri/runtime-x86_64", home);
|
||||
snprintf(path, sizeof path, "/tmp/x86_64-cross-tools:%s",
|
||||
old_path ? old_path : "/usr/local/bin:/usr/bin:/bin");
|
||||
if (setenv("ARCH", "x86_64", 1) ||
|
||||
setenv("NO_STRIP", "true", 1) ||
|
||||
setenv("APPIMAGE_EXTRACT_AND_RUN", "1", 1) ||
|
||||
setenv("LDAI_RUNTIME_FILE", runtime, 1) ||
|
||||
setenv("LD_GTK_LIBRARY_PATH", "/usr/lib/x86_64-linux-gnu", 1) ||
|
||||
setenv("DEB_HOST_MULTIARCH", "x86_64-linux-gnu", 1) ||
|
||||
setenv("PKG_CONFIG_ALLOW_CROSS", "1", 1) ||
|
||||
setenv("PKG_CONFIG_LIBDIR", "/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig", 1) ||
|
||||
setenv("PATH", path, 1)) {
|
||||
perror("setenv");
|
||||
return 127;
|
||||
}
|
||||
char **args = calloc((size_t)argc + 1, sizeof *args);
|
||||
if (args == NULL) { perror("calloc"); return 127; }
|
||||
args[0] = target;
|
||||
for (int i = 1; i < argc; i++) args[i] = argv[i];
|
||||
execv(target, args);
|
||||
perror(target);
|
||||
return 127;
|
||||
}
|
||||
EOF
|
||||
cc -O2 -Wall -Wextra -o "$launcher_bin" "$launcher_src"
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
export PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
|
||||
export CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
|
||||
export CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++
|
||||
bundle="${CARGO_TARGET_DIR:-target}/x86_64-unknown-linux-gnu/release/bundle"
|
||||
rm -rf "$bundle/appimage" "$bundle/deb"
|
||||
tauri build --target x86_64-unknown-linux-gnu --bundles appimage,deb
|
||||
fi
|
||||
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
||||
;;
|
||||
windows)
|
||||
rustup target add x86_64-pc-windows-msvc >/dev/null
|
||||
bundle="${CARGO_TARGET_DIR:-target}/x86_64-pc-windows-msvc/release/bundle/nsis"
|
||||
rm -rf "$bundle"
|
||||
tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
|
||||
cp "$bundle"/*-setup.exe ../../../dist-gui/
|
||||
;;
|
||||
*) echo "unknown platform: $platform" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
(
|
||||
cd ../../../dist-gui
|
||||
for asset in *; do
|
||||
[ -f "$asset" ] || continue
|
||||
case "$asset" in
|
||||
*.sha256) ;;
|
||||
*)
|
||||
sha256sum "$asset" > "$asset.sha256"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
)
|
||||
|
||||
ls -1 ../../../dist-gui
|
||||
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env node
|
||||
// Verifies that the English dictionary covers all user-facing interface strings.
|
||||
//
|
||||
// Static zero-dependency scanner: reads the UI files (JS and HTML) and the
|
||||
// pt-BR messages emitted by the Rust core (errors, diagnostics, engine status),
|
||||
// failing whenever a string looks like Portuguese and lacks a translation. Also
|
||||
// rejects duplicate keys in the dictionary (in a JS object literal the last key wins silently).
|
||||
//
|
||||
// Usage: node scripts/check-ui-i18n.mjs
|
||||
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
|
||||
const rel = (p) => relative(root, p).replaceAll('\\', '/');
|
||||
|
||||
// ---------------------------------------------------------------- dictionary
|
||||
|
||||
const dictPath = join(root, 'apps/gui/ui/i18n-en.js');
|
||||
const dictSource = readFileSync(dictPath, 'utf8');
|
||||
|
||||
const keyRe = /^\s*(['"])((?:(?!\1).)*)\1\s*:/gm;
|
||||
const seen = new Map();
|
||||
const duplicates = [];
|
||||
let m;
|
||||
while ((m = keyRe.exec(dictSource))) {
|
||||
const key = m[2];
|
||||
const line = dictSource.slice(0, m.index).split('\n').length;
|
||||
if (seen.has(key)) duplicates.push({ key, first: seen.get(key), again: line });
|
||||
else seen.set(key, line);
|
||||
}
|
||||
|
||||
if (duplicates.length) {
|
||||
console.error('Duplicate keys in apps/gui/ui/i18n-en.js:');
|
||||
for (const d of duplicates) console.error(` ${JSON.stringify(d.key)} (lines ${d.first} and ${d.again})`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
const { EN } = await import(new URL('../apps/gui/ui/i18n-en.js', import.meta.url).href);
|
||||
|
||||
const exact = new Set(Object.keys(EN).filter((k) => !k.includes('{n}')));
|
||||
const patterns = Object.keys(EN)
|
||||
.filter((k) => k.includes('{n}'))
|
||||
.map((k) => {
|
||||
const parts = k.split('{n}').map((p) => p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||
return new RegExp('^' + parts.join('(.+?)') + '$', 's');
|
||||
});
|
||||
|
||||
function translated(text) {
|
||||
if (exact.has(text)) return true;
|
||||
return patterns.some((re) => re.test(text));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- heuristic
|
||||
|
||||
const ACCENT = /[áàâãäéèêëíìîïóòôõöúùûüçÁÀÂÃÄÉÈÊËÍÌÎÏÓÒÔÕÖÚÙÛÜÇ]/;
|
||||
const WORDS = [
|
||||
'pilar', 'pilares', 'ameaças', 'escopo', 'autorizado', 'tema', 'idioma', 'esqueleto', 'analisar',
|
||||
'atualizar', 'status', 'alerta', 'alertas', 'registros', 'detector', 'protocolo', 'anomalias',
|
||||
'fusão', 'evidências', 'conexões', 'sessões', 'persistentes', 'desenvolvimento', 'prova', 'opaca',
|
||||
'pesquisa', 'determinística', 'armazenamento', 'implementados', 'matemáticos', 'quatro', 'pacote',
|
||||
'enviado', 'fora', 'próprias', 'autorização', 'explícita', 'serviço', 'indisponível', 'selecionado',
|
||||
'analisando', 'concluída', 'falha', 'verificando', 'versão', 'recente', 'disponível', 'possível',
|
||||
'carregados', 'navegação', 'principal', 'recentes', 'futuro', 'carregará', 'sustenta', 'motor',
|
||||
'detecção', 'próximas', 'specs', 'fechar', 'claro', 'escuro', 'rede', 'redes', 'interface', 'interfaces', 'captura', 'capturar', 'modo', 'ativar', 'ativa',
|
||||
'desativar', 'iniciar', 'parar', 'nenhum', 'nenhuma', 'selecione', 'selecionada', 'canal',
|
||||
'sinal', 'segurança', 'clientes', 'cliente', 'oculta', 'oculto', 'privilégio', 'administrador',
|
||||
'necessário', 'pra', 'própria', 'próprias', 'autorização', 'responsável', 'sessão', 'trilha',
|
||||
'auditoria', 'registrada', 'registrado', 'local', 'adaptador', 'compatível', 'recarregue',
|
||||
'escutando', 'levar', 'segundos', 'enquanto', 'canais', 'varridos', 'erro', 'desconhecido',
|
||||
'desconhecida', 'não', 'sim', 'mais', 'menos', 'uma', 'uns', 'umas', 'aqui', 'onde', 'quando',
|
||||
'como', 'qual', 'para', 'com', 'sem', 'dos', 'das', 'por', 'que', 'checando', 'checar',
|
||||
'atualização', 'atualizado', 'trocar', 'possível', 'quebrável', 'quebrado', 'rompida',
|
||||
'corrompida', 'inválido', 'inválida', 'entrada', 'dicionário', 'quadros', 'quantidade',
|
||||
'injetar', 'encontrado', 'encontrada', 'suportada', 'permissão', 'insuficiente', 'instale',
|
||||
'root', 'indisponível', 'plataforma', 'criada', 'ainda', 'ferramenta', 'ferramentas',
|
||||
'exportar', 'importar', 'imprimir', 'baixar', 'recomendado', 'recomendada', 'aberta',
|
||||
'aberto', 'inconclusivo', 'inconclusiva', 'capturado', 'capturada', 'conhecido',
|
||||
'conhecida', 'opcional', 'opcionais', 'fechar', 'motivo', 'linha', 'tempo', 'quadro',
|
||||
'tentativas', 'ambas',
|
||||
];
|
||||
|
||||
const BRAND = /Acauã|Português|English/gi;
|
||||
const isPortuguese = (raw) => {
|
||||
const s = raw.replace(BRAND, ' ');
|
||||
return ACCENT.test(s) || WORDS.some((w) => new RegExp(`(^|[^a-zà-ú])${w}([^a-zà-ú]|$)`, 'i').test(s));
|
||||
};
|
||||
|
||||
const ALLOW = new Set([
|
||||
'Acauã', 'Português', 'English', 'Auto', 'Wi-Fi', 'Zeek', 'DuckDB', 'API', 'JSON', 'CLI',
|
||||
'MIT', 'Apache-2.0', 'OK',
|
||||
]);
|
||||
const ALLOW_PREFIX = ['http://', 'https://', 'data:', 'file:', '#', '.', '/'];
|
||||
const ALLOW_EXACT = new Set(['—', '-', '·']);
|
||||
|
||||
function suspicious(text) {
|
||||
const s = text.replace(/\s+/g, ' ').trim();
|
||||
if (!s || s.length < 2) return false;
|
||||
if (ALLOW.has(s) || ALLOW_EXACT.has(s)) return false;
|
||||
if (ALLOW_PREFIX.some((p) => s.startsWith(p))) return false;
|
||||
if (!isPortuguese(s)) return false;
|
||||
// Technical tokens: CSS selectors, classes, extensions, expressions
|
||||
if (/^[.#\w-]+(\s*[,>+~]\s*[.#\w-]+)+$/.test(s)) return false;
|
||||
if (/^[a-z0-9_.:/@%{}$-]+$/i.test(s) && !ACCENT.test(s)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- extractors
|
||||
|
||||
function jsStrings(source, file, baseLine = 1) {
|
||||
const out = [];
|
||||
let i = 0;
|
||||
let line = baseLine;
|
||||
let prev = '';
|
||||
const skipRegex = () => {
|
||||
i++;
|
||||
let inClass = false;
|
||||
while (i < source.length) {
|
||||
const ch = source[i];
|
||||
if (ch === '\\') { i += 2; continue; }
|
||||
if (ch === '[') inClass = true;
|
||||
else if (ch === ']') inClass = false;
|
||||
else if (ch === '/' && !inClass) { i++; break; }
|
||||
else if (ch === '\n') line++;
|
||||
i++;
|
||||
}
|
||||
while (i < source.length && /[a-z]/i.test(source[i])) i++;
|
||||
};
|
||||
while (i < source.length) {
|
||||
const c = source[i];
|
||||
if (c === '\n') { line++; i++; prev = '\n'; continue; }
|
||||
if (c === ' ' || c === '\t' || c === '\r') { i++; continue; }
|
||||
if (c === '/' && source[i + 1] === '/') {
|
||||
while (i < source.length && source[i] !== '\n') i++;
|
||||
continue;
|
||||
}
|
||||
if (c === '/' && source[i + 1] === '*') {
|
||||
i += 2;
|
||||
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) { if (source[i] === '\n') line++; i++; }
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (c === '/' && (/[=(:,[!&|?{};+\-*%^~<>]/.test(prev) || prev === 'return')) {
|
||||
skipRegex();
|
||||
prev = ')';
|
||||
continue;
|
||||
}
|
||||
if (c === "'" || c === '"') {
|
||||
const quote = c;
|
||||
const startLine = line;
|
||||
let value = '';
|
||||
i++;
|
||||
while (i < source.length) {
|
||||
const ch = source[i];
|
||||
if (ch === '\\') {
|
||||
const n = source[i + 1];
|
||||
if (n === 'n') value += '\n';
|
||||
else if (n === 't') value += '\t';
|
||||
else if (n === 'r') value += '\r';
|
||||
else if (n === '\n') line++;
|
||||
else value += n ?? '';
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === quote) { i++; break; }
|
||||
if (ch === '\n') line++;
|
||||
value += ch;
|
||||
i++;
|
||||
}
|
||||
out.push({ text: value, file, line: startLine });
|
||||
prev = ')';
|
||||
continue;
|
||||
}
|
||||
if (c === '`') {
|
||||
const startLine = line;
|
||||
let value = '';
|
||||
i++;
|
||||
while (i < source.length) {
|
||||
const ch = source[i];
|
||||
if (ch === '\\') {
|
||||
const n = source[i + 1];
|
||||
if (n === 'n') value += '\n';
|
||||
else if (n === 't') value += '\t';
|
||||
else if (n === '\n') line++;
|
||||
else value += n ?? '';
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === '`') { i++; break; }
|
||||
if (ch === '$' && source[i + 1] === '{') {
|
||||
value += '{n}';
|
||||
i += 2;
|
||||
const exprStart = i;
|
||||
const exprLine = line;
|
||||
let depth = 1;
|
||||
while (i < source.length && depth > 0) {
|
||||
if (source[i] === '{') depth++;
|
||||
else if (source[i] === '}') depth--;
|
||||
else if (source[i] === '\n') line++;
|
||||
i++;
|
||||
}
|
||||
out.push(...jsStrings(source.slice(exprStart, i - 1), file, exprLine));
|
||||
continue;
|
||||
}
|
||||
if (ch === '\n') line++;
|
||||
value += ch;
|
||||
i++;
|
||||
}
|
||||
out.push({ text: value, file, line: startLine });
|
||||
prev = ')';
|
||||
continue;
|
||||
}
|
||||
prev = c;
|
||||
i++;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function htmlStrings(source, file, baseLine = 1) {
|
||||
const out = [];
|
||||
const lineAt = (idx) => baseLine + source.slice(0, idx).split('\n').length - 1;
|
||||
// Text between tags (including leading and trailing text nodes)
|
||||
let last = 0;
|
||||
for (const m of source.matchAll(/<[^>]*>/g)) {
|
||||
out.push({ text: source.slice(last, m.index), file, line: lineAt(last) });
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
out.push({ text: source.slice(last), file, line: lineAt(last) });
|
||||
// Translatable attributes (single or double quotes)
|
||||
for (const m of source.matchAll(/\b(placeholder|title|aria-label|alt|data-empty)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
||||
out.push({ text: m[2] ?? m[3], file, line: lineAt(m.index) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Extracts pt-BR messages from the Rust core: #[error("...")], .push("...".into()),
|
||||
// "...".into(), and format!("...", ...). Placeholders ({name}, {0}, {} etc.) are
|
||||
// normalized to {n}, matching the dictionary format.
|
||||
function rustLabels(source, file) {
|
||||
const out = [];
|
||||
const lineAt = (idx) => source.slice(0, idx).split('\n').length;
|
||||
const norm = (s) => s.replace(/\{[^}]*\}/g, '{n}');
|
||||
const grab = (re) => {
|
||||
for (const m of source.matchAll(re)) {
|
||||
out.push({ text: norm(m[1]), file, line: lineAt(m.index) });
|
||||
}
|
||||
};
|
||||
grab(/#\[error\(\s*"((?:[^"\\]|\\.)*)"\s*\)\]/g);
|
||||
grab(/"((?:[^"\\]|\\.)*)"\s*\.into\(\)/g);
|
||||
grab(/format!\(\s*"((?:[^"\\]|\\.)*)"/g);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- collection
|
||||
|
||||
const uiFiles = readdirSync(join(root, 'apps/gui/ui'))
|
||||
.filter((f) => f.endsWith('.js') && f !== 'i18n-en.js' && f !== 'vendor')
|
||||
.map((f) => join(root, 'apps/gui/ui', f));
|
||||
uiFiles.push(join(root, 'apps/gui/ui/index.html'));
|
||||
|
||||
const rustFiles = [
|
||||
'crates/acaua-core/src/error.rs',
|
||||
'crates/acaua-core/src/doctor.rs',
|
||||
'crates/acaua-core/src/storage.rs',
|
||||
'crates/acaua-core/src/zeek.rs',
|
||||
'crates/acaua-core/src/update.rs',
|
||||
].map((f) => join(root, f));
|
||||
|
||||
const found = [];
|
||||
for (const file of uiFiles) {
|
||||
const source = readFileSync(file, 'utf8');
|
||||
if (file.endsWith('.html')) found.push(...htmlStrings(source, file));
|
||||
else {
|
||||
for (const str of jsStrings(source, file)) {
|
||||
if (/<[a-z!/][^>]*>/i.test(str.text)) found.push(...htmlStrings(str.text, file, str.line));
|
||||
else found.push(str);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const file of rustFiles) found.push(...rustLabels(readFileSync(file, 'utf8'), file));
|
||||
|
||||
const missing = [];
|
||||
const seenMissing = new Set();
|
||||
for (const { text, file, line } of found) {
|
||||
if (!suspicious(text)) continue;
|
||||
const key = text.replace(/\s+/g, ' ').trim();
|
||||
if (translated(key)) continue;
|
||||
const id = `${key}\u0000${rel(file)}\u0000${line}`;
|
||||
if (seenMissing.has(id)) continue;
|
||||
seenMissing.add(id);
|
||||
missing.push({ text: key, file: rel(file), line });
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
console.error('Untranslated Portuguese strings in apps/gui/ui/i18n-en.js:\n');
|
||||
for (const x of missing) console.error(` ${x.file}:${x.line} ${JSON.stringify(x.text)}`);
|
||||
console.error(`\n${missing.length} untranslated string(s).`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (!process.exitCode) {
|
||||
console.log(`i18n ok: ${found.length} strings checked, ${Object.keys(EN).length} dictionary keys.`);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local verification: runs the same fast checks as CI before pushing.
|
||||
#
|
||||
# scripts/check.sh fast (~1 min): fmt, clippy, i18n, scripts, workflows, semver
|
||||
# scripts/check.sh --full also runs workspace tests and cargo audit
|
||||
#
|
||||
# CI takes 5 to 30 minutes per run; almost everything that breaks CI is caught
|
||||
# here in seconds.
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
full=0
|
||||
[ "${1:-}" = "--full" ] || [ "${1:-}" = "--completo" ] && full=1
|
||||
failures=()
|
||||
step() { # step "name" command...
|
||||
local name="$1"; shift
|
||||
printf '\n\033[1m== %s\033[0m\n' "$name"
|
||||
if "$@"; then printf '\033[32mok\033[0m %s\n' "$name"; else printf '\033[31mFAILED\033[0m %s\n' "$name"; failures+=("$name"); fi
|
||||
}
|
||||
|
||||
step "formatting (cargo fmt)" cargo fmt --all -- --check
|
||||
# Same flags as CI: warnings are treated as errors across all targets, including tests
|
||||
step "clippy (workspace, all targets)" env RUSTFLAGS="-D warnings" cargo clippy --workspace --all-targets
|
||||
|
||||
check_forbidden_terms() {
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
|
||||
terms = [
|
||||
'Fast' + 'API',
|
||||
'Click' + 'House',
|
||||
'latest' + '.json',
|
||||
'updater' + '.pubkey',
|
||||
'createUpdater' + 'Artifacts',
|
||||
'plugin' + '-updater',
|
||||
]
|
||||
skip_dirs = {'.git', 'target', '.wolf', '.claude', 'dist-gui'}
|
||||
allowed = {Path('docs/architecture.md')}
|
||||
hits = []
|
||||
for path in Path('.').rglob('*'):
|
||||
if not path.is_file() or any(part in skip_dirs for part in path.parts):
|
||||
continue
|
||||
if path in allowed:
|
||||
continue
|
||||
try:
|
||||
text = path.read_text(errors='ignore')
|
||||
except OSError:
|
||||
continue
|
||||
for term in terms:
|
||||
if term in text:
|
||||
hits.append(f'{path}: {term}')
|
||||
if hits:
|
||||
print('\n'.join(hits))
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
step "UI translation coverage" node scripts/check-ui-i18n.mjs
|
||||
step "bash script syntax" bash -c 'for f in scripts/*.sh .githooks/*; do bash -n "$f" || exit 1; done'
|
||||
step "workflows and templates (valid YAML)" python3 -c "
|
||||
import glob, yaml
|
||||
for f in glob.glob('.gitea/**/*.yml', recursive=True):
|
||||
yaml.safe_load(open(f))
|
||||
"
|
||||
step "brand assets" test -s logo.png -a -s apps/gui/src-tauri/icons/source.png -a -s apps/gui/src-tauri/icons/icon.ico
|
||||
step "consistent Semantic Versioning 2.0.0" python3 -c "
|
||||
import json, re, sys
|
||||
from pathlib import Path
|
||||
|
||||
semver_re = re.compile(
|
||||
r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
|
||||
r'(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?'
|
||||
r'(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
|
||||
)
|
||||
|
||||
def cargo_version(path):
|
||||
m = re.search(r'^version\s*=\s*\"([^\"]+)\"', Path(path).read_text(), re.M)
|
||||
if not m:
|
||||
sys.exit(f'version not found in {path}')
|
||||
return m.group(1)
|
||||
|
||||
versions = [
|
||||
cargo_version('Cargo.toml'),
|
||||
cargo_version('apps/gui/src-tauri/Cargo.toml'),
|
||||
json.load(open('apps/gui/src-tauri/tauri.conf.json'))['version'],
|
||||
]
|
||||
if len(set(versions)) != 1:
|
||||
sys.exit(f'version mismatch: Cargo.toml={versions[0]}, src-tauri={versions[1]}, tauri.conf={versions[2]}')
|
||||
if not semver_re.match(versions[0]):
|
||||
sys.exit(f'invalid SemVer 2.0.0 version: {versions[0]}')
|
||||
print(versions[0])
|
||||
"
|
||||
step "forbidden stack terms" check_forbidden_terms
|
||||
if [ -f apps/gui/src-tauri/Cargo.toml ] && pkg-config --exists webkit2gtk-4.1 2>/dev/null; then
|
||||
step "desktop app (cargo check)" env RUSTFLAGS="" cargo check --manifest-path apps/gui/src-tauri/Cargo.toml
|
||||
else
|
||||
printf '\n(desktop app: skipped, WebKitGTK development libraries not installed; built in CI)\n'
|
||||
fi
|
||||
|
||||
if [ "$full" = 1 ]; then
|
||||
step "tests (cargo test --workspace)" cargo test --workspace
|
||||
step "dependency audit (cargo audit)" cargo audit
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
if [ ${#failures[@]} -eq 0 ]; then
|
||||
printf '\033[32mall checks passed\033[0m: ready to push.\n'
|
||||
else
|
||||
printf '\033[31mfailed:\033[0m %s\n' "${failures[*]}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generates the base Tauri icon from the official project logo.
|
||||
|
||||
The official logo is `logo.png` in the repository root (RGBA, transparent
|
||||
background, suitable for both light and dark themes). It is not modified here.
|
||||
Output:
|
||||
apps/gui/src-tauri/icons/source.png 1024 px artwork centered over a rounded
|
||||
dark background; CI and `npx @tauri-apps/cli icon`
|
||||
derive all remaining icon formats from it.
|
||||
|
||||
Usage: python3 scripts/generate-logo.py
|
||||
"""
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
||||
ICONS_DIR = os.path.join(ROOT, "apps/gui/src-tauri/icons")
|
||||
ICON_BG = (10, 16, 28) # dark navy background matching the original artwork
|
||||
|
||||
|
||||
def crop_square(im, margin=0.04):
|
||||
"""Crops to visible pixels (opacity > 16, ignoring faint artifacts) and centers in a square."""
|
||||
box = im.getchannel("A").point(lambda v: 255 if v > 16 else 0).getbbox()
|
||||
im = im.crop(box)
|
||||
w, h = im.size
|
||||
side = round(max(w, h) * (1 + 2 * margin))
|
||||
canvas = Image.new("RGBA", (side, side), (0, 0, 0, 0))
|
||||
canvas.paste(im, ((side - w) // 2, (side - h) // 2), im)
|
||||
return canvas
|
||||
|
||||
|
||||
def main():
|
||||
logo = crop_square(Image.open(os.path.join(ROOT, "logo.png")).convert("RGBA"))
|
||||
|
||||
side = 1024
|
||||
bg = Image.new("RGBA", (side, side), ICON_BG + (255,))
|
||||
art = logo.resize((round(side * 0.82),) * 2, Image.LANCZOS)
|
||||
bg.paste(art, ((side - art.width) // 2, (side - art.height) // 2), art)
|
||||
mask = Image.new("L", (side, side), 0)
|
||||
ImageDraw.Draw(mask).rounded_rectangle((0, 0, side - 1, side - 1), radius=round(side * 0.2237), fill=255)
|
||||
icon = Image.new("RGBA", (side, side), (0, 0, 0, 0))
|
||||
icon.paste(bg, (0, 0), mask)
|
||||
os.makedirs(ICONS_DIR, exist_ok=True)
|
||||
dest = os.path.join(ICONS_DIR, "source.png")
|
||||
icon.save(dest, optimize=True)
|
||||
os.chmod(dest, 0o644)
|
||||
print("icons/source.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Enables the repository Git hooks (.githooks/pre-push and .githooks/commit-msg).
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
git config core.hooksPath .githooks
|
||||
echo "hooks enabled: git commit validates Conventional Commits in English and git push runs scripts/check.sh (disable with: git config --unset core.hooksPath)"
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the CLI for a target platform and packages it as expected by the updater:
|
||||
# acaua-<tag>-<platform>.tar.gz alongside a .sha256 checksum file.
|
||||
#
|
||||
# scripts/package-cli.sh v1.0.3 linux-x86_64 x86_64-unknown-linux-gnu
|
||||
#
|
||||
# Supported platforms: linux-x86_64 and linux-aarch64. When cross-compiling
|
||||
# from a non-x86_64 host, uses gcc-x86-64-linux-gnu. Writes the archives to the
|
||||
# repository root and prints their filenames.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
tag=${1:?usage: package-cli.sh <tag> <platform> <rust-target>}
|
||||
platform=${2:?usage: package-cli.sh <tag> <platform> <rust-target>}
|
||||
target=${3:?usage: package-cli.sh <tag> <platform> <rust-target>}
|
||||
|
||||
case "$target" in
|
||||
x86_64-unknown-linux-gnu)
|
||||
if [ "$(uname -m)" != x86_64 ]; then
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
|
||||
export CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
rustup target add "$target" >/dev/null 2>&1 || true
|
||||
cargo build --release --locked -p acaua-cli --target "$target"
|
||||
|
||||
name="acaua-${tag}-${platform}"
|
||||
rm -rf "$name"; mkdir "$name"
|
||||
cp "${CARGO_TARGET_DIR:-target}/${target}/release/acaua" "$name/"
|
||||
cp README.md LICENSE-MIT LICENSE-APACHE "$name/"
|
||||
|
||||
tar czf "$name.tar.gz" "$name"
|
||||
asset="$name.tar.gz"
|
||||
rm -rf "$name"
|
||||
sha256sum "$asset" > "$asset.sha256"
|
||||
echo "$asset"
|
||||
echo "$asset.sha256"
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prints release notes for a given tag: extracts the matching CHANGELOG.md
|
||||
# section, or falls back to the commit log since the previous tag.
|
||||
#
|
||||
# scripts/release-notes.sh v1.2.3
|
||||
set -euo pipefail
|
||||
|
||||
tag=${1:?usage: release-notes.sh <tag>}
|
||||
version=${tag#v}
|
||||
|
||||
section=""
|
||||
if [ -f CHANGELOG.md ]; then
|
||||
section=$(awk -v head="## [$version]" '
|
||||
index($0, head) == 1 { found = 1; next }
|
||||
found && /^## \[/ { exit }
|
||||
found && /^\[[^]]+\]: / { next }
|
||||
found { print }
|
||||
' CHANGELOG.md)
|
||||
fi
|
||||
|
||||
if [ -n "$(printf '%s' "$section" | tr -d '[:space:]')" ]; then
|
||||
printf '%s\n' "$section"
|
||||
else
|
||||
previous=$(git describe --tags --abbrev=0 "$tag^" 2>/dev/null || true)
|
||||
echo "## Changes${previous:+ since $previous}"
|
||||
echo
|
||||
git log --no-merges --format='- %s (%h)' "${previous:+$previous..}$tag"
|
||||
fi
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Uploads assets to a Gitea release, replacing any existing asset with the same
|
||||
# filename so re-running a CI job never fails on duplicate attachments.
|
||||
#
|
||||
# API=https://git.example.com/api/v1/repos/owner/repo TOKEN=... \
|
||||
# scripts/release-upload.sh v1.2.3 file1 file2
|
||||
set -euo pipefail
|
||||
|
||||
: "${API:?set API (Gitea repository API URL)}"
|
||||
: "${TOKEN:?set TOKEN}"
|
||||
tag=${1:?usage: release-upload.sh <tag> <file>...}
|
||||
shift
|
||||
|
||||
auth=(-H "Authorization: token $TOKEN")
|
||||
id=$(curl -fsS "${auth[@]}" "$API/releases/tags/$tag" | jq -r .id)
|
||||
|
||||
for file in "$@"; do
|
||||
name=$(basename "$file")
|
||||
encoded=$(jq -rn --arg n "$name" '$n|@uri')
|
||||
for old in $(curl -fsS "${auth[@]}" "$API/releases/$id/assets" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id'); do
|
||||
curl -fsS -X DELETE "${auth[@]}" "$API/releases/$id/assets/$old"
|
||||
done
|
||||
curl -fsS -X POST "${auth[@]}" -F "attachment=@$file" "$API/releases/$id/assets?name=$encoded" >/dev/null
|
||||
echo "uploaded: $name"
|
||||
done
|
||||