diff --git a/.dockerignore b/.dockerignore index 1c39b5a..03ad31f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,8 @@ .git target -apps/gui/src-tauri docs scripts +!scripts/fetch-libduckdb.sh research data .acaua @@ -11,3 +11,4 @@ data .claude .wolf .superpowers +.env diff --git a/.env.example b/.env.example index 4a1191d..29bc769 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,9 @@ -# Interface captured by the Compose Zeek service. Use only on an authorized network. +# Interface captured by the Zeek service. Use only on a network you are authorized to monitor. ZEEK_INTERFACE=eth0 +# Acauã image tag: latest (last release), X.Y.Z / X.Y (pinned), or edge (main). +ACAUA_VERSION=latest +# Host address and port of the dashboard. Keep it on loopback unless an +# authenticating reverse proxy sits in front of it. +ACAUA_PUBLISH=127.0.0.1:8787 +# Zeek image, pinned to an exact version. +ZEEK_IMAGE=zeek/zeek:9.0.1 diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9ee5017..9e0c209 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,11 +2,9 @@ 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'] + paths-ignore: ['**.md', 'docs/**', 'LICENSE*', '.gitignore', '.githooks/**', '.wolf/**', '.claude/**', '.opencode/**', '.codex/**', '.cursor/**', '.superpowers/**', '.superset/**', '.gitea/ISSUE_TEMPLATE/**', 'scripts/release-notes.sh', '.gitea/workflows/release.yml'] + workflow_call: concurrency: group: ci-${{ gitea.ref }} @@ -22,6 +20,11 @@ env: CARGO_HOME: /opt/hostedtoolcache/cargo RUSTUP_HOME: /opt/hostedtoolcache/rustup NPM_CONFIG_CACHE: /opt/hostedtoolcache/npm-cache + # Prebuilt libduckdb (scripts/fetch-libduckdb.sh), cached on the persistent + # runner volume. Every CI job runs on linux-arm64. + DUCKDB_LIB_DIR: /opt/hostedtoolcache/duckdb/linux-arm64 + DUCKDB_INCLUDE_DIR: /opt/hostedtoolcache/duckdb/linux-arm64 + LD_LIBRARY_PATH: /opt/hostedtoolcache/duckdb/linux-arm64 jobs: lint: @@ -39,6 +42,10 @@ jobs: node-version: 22 - name: UI translation coverage run: node scripts/check-ui-i18n.mjs + - name: Prebuilt libduckdb + run: | + command -v unzip >/dev/null || scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/unzip unzip + scripts/fetch-libduckdb.sh "$DUCKDB_LIB_DIR" - run: cargo fmt --all -- --check - run: cargo clippy --workspace --all-targets @@ -51,6 +58,10 @@ jobs: steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@stable + - name: Prebuilt libduckdb + run: | + command -v unzip >/dev/null || scripts/apt-install.sh /opt/hostedtoolcache/apt-cache/unzip unzip + scripts/fetch-libduckdb.sh "$DUCKDB_LIB_DIR" - run: cargo test --workspace audit: @@ -67,22 +78,16 @@ jobs: command -v cargo-audit >/dev/null || cargo install cargo-audit --locked cargo audit - gui: - name: Check desktop app (Linux) + image: + name: Docker image (build and smoke test) + if: ${{ gitea.event_name == 'pull_request' }} 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: "" + - name: Build + run: docker build -t "acaua-ci:${{ gitea.run_id }}" . + - name: Smoke test + run: scripts/smoke-image.sh "acaua-ci:${{ gitea.run_id }}" + - name: Clean up + if: always() + run: docker rmi -f "acaua-ci:${{ gitea.run_id }}" >/dev/null 2>&1 || true diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1177210..6cf2c0f 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,25 +1,146 @@ 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). +# Publishes the multi-arch Acauã image (linux/amd64 + linux/arm64) to the Gitea +# container registry. A merge into main publishes `edge`; a v* tag re-points the +# image already built for the same inputs to X.Y.Z, X.Y and latest (no rebuild) +# and creates the Gitea Release. Needs the secret REGISTRY_TOKEN (Gitea token +# with write:package) visible to this repository. on: push: + branches: [main] tags: ['v*'] + workflow_dispatch: + +concurrency: + group: release-${{ gitea.ref }} + cancel-in-progress: false 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 + REGISTRY: git.lucasrguerra.dev.br jobs: - release: - name: Create release - runs-on: ubuntu-latest + # The image is built only from these paths, so their tree hashes identify it. + # When inputs- already exists (docs-only merge, workflow change, or a tag + # on a commit main already published), nothing is built or tested again. + plan: + name: Plan (reuse the published image?) + runs-on: linux-arm64 + outputs: + inputs: ${{ steps.plan.outputs.inputs }} + reuse: ${{ steps.plan.outputs.reuse }} + image: ${{ steps.plan.outputs.image }} + steps: + - uses: actions/checkout@v7 + - id: plan + env: + REPO: ${{ gitea.repository }} + run: | + inputs=$(git rev-parse HEAD:Dockerfile HEAD:.dockerignore HEAD:Cargo.toml HEAD:Cargo.lock \ + HEAD:crates HEAD:apps/gui/ui HEAD:logo.png HEAD:scripts/fetch-libduckdb.sh | sha256sum | cut -c1-16) + ns="${REPO,,}" + token=$(curl -fsS "https://$REGISTRY/v2/token?service=container_registry&scope=repository:$ns:pull" | jq -r .token) + code=$(curl -s -o /dev/null -w '%{http_code}' -I \ + -H "Authorization: Bearer $token" \ + -H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json' \ + "https://$REGISTRY/v2/$ns/manifests/inputs-$inputs") + echo "inputs-$inputs -> HTTP $code" + reuse=false; [ "$code" = 200 ] && reuse=true + echo "inputs=$inputs" >> "$GITHUB_OUTPUT" + echo "reuse=$reuse" >> "$GITHUB_OUTPUT" + echo "image=$REGISTRY/$ns" >> "$GITHUB_OUTPUT" + + ci: + needs: plan + if: ${{ needs.plan.outputs.reuse != 'true' }} + uses: ./.gitea/workflows/ci.yml + + # Each architecture is built on a native runner (no QEMU) and pushed under a + # per-architecture tag; publish joins them into one manifest list. + build: + name: Build image (${{ matrix.arch }}) + needs: [plan, ci] + if: ${{ needs.plan.outputs.reuse != 'true' }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - { arch: amd64, platform: linux/amd64, runner: linux-amd64 } + - { arch: arm64, platform: linux/arm64, runner: linux-arm64 } + steps: + - uses: actions/checkout@v7 + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Build and push (per architecture) + uses: docker/build-push-action@v7 + with: + context: . + platforms: ${{ matrix.platform }} + push: true + tags: ${{ needs.plan.outputs.image }}:build-${{ gitea.sha }}-${{ matrix.arch }} + cache-from: type=registry,ref=${{ needs.plan.outputs.image }}:buildcache-${{ matrix.arch }} + cache-to: type=registry,ref=${{ needs.plan.outputs.image }}:buildcache-${{ matrix.arch }},mode=max + # Runs the image that was just pushed, on its own architecture. + - name: Smoke test + run: scripts/smoke-image.sh "${{ needs.plan.outputs.image }}:build-${{ gitea.sha }}-${{ matrix.arch }}" + + publish: + name: Publish tags + needs: [plan, build] + # build is skipped, not successful, when the image is reused. + if: ${{ !cancelled() && needs.plan.result == 'success' && (needs.build.result == 'success' || needs.plan.outputs.reuse == 'true') }} + runs-on: linux-arm64 + steps: + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Tags + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ needs.plan.outputs.image }} + flavor: latest=false + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(gitea.ref, 'refs/tags/v') && !contains(gitea.ref_name, '-') }} + type=raw,value=edge,enable=${{ gitea.ref == 'refs/heads/main' }} + type=sha,format=long + - name: Apply tags + run: | + image='${{ needs.plan.outputs.image }}' + tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') + inputs='${{ needs.plan.outputs.inputs }}' + if [ '${{ needs.plan.outputs.reuse }}' = true ]; then + docker buildx imagetools create $tags "$image:inputs-$inputs" + else + docker buildx imagetools create $tags -t "$image:inputs-$inputs" \ + "$image:build-${{ gitea.sha }}-amd64" "$image:build-${{ gitea.sha }}-arm64" + fi + - name: Check architectures + run: | + docker buildx imagetools inspect "${{ needs.plan.outputs.image }}:inputs-${{ needs.plan.outputs.inputs }}" \ + --format '{{json .Manifest}}' \ + | jq -r '.manifests[] | select(.platform.os == "linux") | "\(.platform.os)/\(.platform.architecture)"' \ + | tee platforms.txt + grep -qx linux/amd64 platforms.txt && grep -qx linux/arm64 platforms.txt + - name: Summary + run: | + { echo "### Acauã image published (linux/amd64, linux/arm64)"; echo; echo '```' + echo "${{ steps.meta.outputs.tags }}"; echo '```'; } >> "$GITHUB_STEP_SUMMARY" + + gitea-release: + name: Gitea release + needs: [plan, publish] + if: ${{ !cancelled() && needs.publish.result == 'success' && startsWith(gitea.ref, 'refs/tags/v') }} + runs-on: linux-arm64 steps: - uses: actions/checkout@v7 with: @@ -29,98 +150,14 @@ jobs: TAG: ${{ gitea.ref_name }} TOKEN: ${{ gitea.token }} API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }} + IMAGE: ${{ needs.plan.outputs.image }} 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 + { scripts/release-notes.sh "$TAG"; echo; echo '```bash'; echo "docker pull $IMAGE:${TAG#v}"; echo '```'; } > 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/* diff --git a/.gitignore b/.gitignore index f13ccb7..be135e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,6 @@ # 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 @@ -37,6 +33,3 @@ research/.ipynb_checkpoints/ CLAUDE.md AGENTS.md GEMINI.md - -# Logo copied into the UI directory by the desktop build script -/apps/gui/ui/logo.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 518543d..965aed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versio ## [Unreleased] +### Changed +- Distribution pivoted to 100% Docker and docker-compose (server daemon and web dashboard); native binary tarballs and desktop installers are discontinued. +- DuckDB is now a mandatory runtime dependency, dynamically linked against the official prebuilt `libduckdb.so` (v1.5.6) with fixed SHA-256 via `scripts/fetch-libduckdb.sh`. +- Upgraded workspace MSRV to Rust 1.88 (required by `duckdb ~1.10506.1`). +- Upgraded container runtime and base image to Debian 13 trixie. + +### Fixed +- Graceful shutdown on `SIGTERM` when running as PID 1 in container environments. + +### Removed +- Tauri desktop application (`apps/gui/src-tauri`) and associated desktop build scripts. +- Self-update flag (`acaua update --apply`) and in-place binary overwrite. Updates are now handled via image pulls (`docker compose pull`). + ## [0.1.0] - 2026-10-31 ### Added diff --git a/CHANGELOG.pt-BR.md b/CHANGELOG.pt-BR.md index 1d38172..dab8a7f 100644 --- a/CHANGELOG.pt-BR.md +++ b/CHANGELOG.pt-BR.md @@ -6,6 +6,19 @@ Formato baseado no [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/); ## [Unreleased] +### Modificado +- Distribuição migrada para 100% Docker e docker-compose (daemon servidor e dashboard web); tarballs binários nativos e instaladores desktop descontinuados. +- DuckDB agora é dependência obrigatória em tempo de execução, linkada dinamicamente contra a `libduckdb.so` oficial pré-compilada (v1.5.6) com SHA-256 fixado via `scripts/fetch-libduckdb.sh`. +- MSRV do workspace atualizado para Rust 1.88 (exigido pelo `duckdb ~1.10506.1`). +- Runtime do container e imagem base atualizados para Debian 13 trixie. + +### Corrigido +- Encerramento limpo em `SIGTERM` ao rodar como PID 1 em ambientes de container. + +### Removido +- Aplicativo desktop Tauri (`apps/gui/src-tauri`) e scripts de build desktop associados. +- Flag de autoatualização (`acaua update --apply`) e sobrescrita de binário em disco. Atualizações agora são feitas via pull de imagem (`docker compose pull`). + ## [0.1.0] - 2026-10-31 ### Adicionado diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9950a46..041797b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,8 +2,8 @@ **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, +Thanks for helping. The project is an NDR engine built around deterministic evidence. +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 @@ -15,26 +15,22 @@ well-scoped design corrections. 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 + commit messages are English. The web dashboard remains bilingual (PT-BR and EN through `i18n-en.js`). ## Workflow ```bash scripts/install-hooks.sh +scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb +export DUCKDB_LIB_DIR=~/.cache/acaua/duckdb LD_LIBRARY_PATH=~/.cache/acaua/duckdb 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 -``` +coverage, shell syntax, workflow YAML, SemVer consistency, DuckDB version pin +verification and forbidden terms inspection. `--full` adds workspace tests and `cargo audit`. ## Research @@ -42,18 +38,6 @@ cargo check -p acaua-core --features duckdb 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 @@ -70,11 +54,11 @@ when the word actually changes, as in `architecture.md` → `arquitetura.md`. 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. +2.0.0. -Pushing a `v*` tag creates a Gitea Release from the matching `CHANGELOG.md` -section and attaches the Linux CLI packages plus the desktop bundles. +Releases are published as multi-arch container images (`linux/amd64` and `linux/arm64`) +to the Gitea registry. Pushing a `v*` tag creates a Gitea Release from the matching +`CHANGELOG.md` section and tags the existing container image. ## License diff --git a/CONTRIBUTING.pt-BR.md b/CONTRIBUTING.pt-BR.md index 0ae54ec..f83e653 100644 --- a/CONTRIBUTING.pt-BR.md +++ b/CONTRIBUTING.pt-BR.md @@ -2,83 +2,66 @@ [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. +Obrigado por ajudar. O projeto é um motor de NDR construído em torno de evidência determinística. +As contribuições mais úteis agora são testes, fixtures, documentação e correções +arquiteturais pontuais. -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`). +1. **Apenas monitoramento autorizado.** O Acauã é software defensivo para redes + próprias ou redes que você tem autorização expressa para observar. Não adicione + mecanismos de coleta que ocultem escopo, retenção ou evidências. +2. **Evidência é parte do recurso.** Uma alteração de detecção deve declarar suas + entradas, reduções em ordem fixa e valores intermediários. Não são aceitas + pontuações sem registro reproduzível de evidência. +3. **Seja transparente sobre o que funciona.** Se um recurso for um stub, limitado + a Linux ou restrito a um log Zeek específico, documente no README, na ajuda da + CLI e nos testes. +4. **Base de código 100% em inglês.** Código-fonte, comentários, scripts, workflows + e mensagens de commit são em inglês. O dashboard web permanece bilíngue (PT-BR + e EN via `i18n-en.js`). -## Fluxo +## Fluxo de trabalho ```bash scripts/install-hooks.sh +scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb +export DUCKDB_LIB_DIR=~/.cache/acaua/duckdb LD_LIBRARY_PATH=~/.cache/acaua/duckdb 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 -``` +`check.sh` executa formatação, clippy com warnings como erro, cobertura de +tradução da UI, sintaxe de shell, YAML de workflows, consistência de SemVer, +validação de pin da DuckDB e termos proibidos. `--full` inclui os testes do +workspace e `cargo audit`. ## 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. +`research/` é um espaço Python isolado para validação científica. Pode utilizar +astropy, scipy, networkx, pandas e notebooks. Nunca deve ser importado pelo runtime +em Rust, copiado para uma release ou exigido para compilar o produto. ## 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. +O texto original da interface é em português. O inglês vem de `apps/gui/ui/i18n-en.js`, +aplicado em runtime por `i18n.js`. Ao adicionar texto visível, execute +`node scripts/check-ui-i18n.mjs` e forneça o par correspondente 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`. +Mantenha os pares: todo arquivo `*.md` e `docs/*` possui uma versão em inglês e +uma em português (`*.pt-BR.md`, `docs/pt-BR/`). Nomes de arquivo são traduzidos +apenas quando a palavra de fato muda, como em `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. +Commits seguem Conventional Commits 1.0.0 em inglês (`feat:`, `fix:`, `chore:`, +`ci:`, `docs:`, `refactor:`, `test:`, `perf:`, `build:`). Versões seguem SemVer 2.0.0. -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. +Releases são publicadas como imagens de container multi-arquitetura (`linux/amd64` +e `linux/arm64`) no registry do Gitea. Enviar uma tag `v*` gera a release no Gitea +a partir do `CHANGELOG.md` e re-aponta as tags da imagem do container existente. ## Licença Ao contribuir, você concorda em licenciar sob [MIT](LICENSE-MIT) **ou** -[Apache-2.0](LICENSE-APACHE), como o resto do projeto. +[Apache-2.0](LICENSE-APACHE), em conformidade com o restante do projeto. diff --git a/Cargo.lock b/Cargo.lock index 030a5b0..c505c2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "acaua-cli" @@ -23,6 +23,7 @@ dependencies = [ name = "acaua-core" version = "0.1.0" dependencies = [ + "chrono", "duckdb", "serde", "serde_json", @@ -122,9 +123,9 @@ dependencies = [ [[package]] name = "arrow" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965156f2220e94dcf676e04322b7b0b41c97e85cb3e1c365b8e56d0f70be999" +checksum = "c943b9bec582e47f8394e44713c3cbfbe645fd049e3a7771585f54849c710fe6" dependencies = [ "arrow-arith", "arrow-array", @@ -140,9 +141,9 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca16fdd1daeb1eccefb42df52ec65448ef33eec72488598675d46dfa9ce58923" +checksum = "8b13b85a6a547a1669114e418f6ab3c3e39c2edb1b8be5ab2889e5739a433ae9" dependencies = [ "arrow-array", "arrow-buffer", @@ -154,9 +155,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3d22a41d3df78b71df0b685af219ae3a94188ab98d6fb29c958eb55c3247f4" +checksum = "7b11eb61b95868592ad6edbd537465a6940eb3c81a875addecec24283301b267" dependencies = [ "ahash", "arrow-buffer", @@ -165,7 +166,6 @@ dependencies = [ "chrono", "half", "hashbrown 0.17.1", - "libc", "num-complex", "num-integer", "num-traits", @@ -173,9 +173,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd46a530bc1a8e763102fc36c1dc0f51f3ffa150c78e7f5658ccb54b1458ade" +checksum = "53ee609a07eb721c2e4a7669d7bb88e2106c560e7dec1219096fecf5eae356d1" dependencies = [ "bytes", "half", @@ -185,9 +185,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa83a0752814a1922d5ee75032524ca9bbfb7f60ee35a67ed4ab8b275260aaf" +checksum = "9575323ffeb4bc6955f39bf8716f734671a32780f1fac9ba6c2891048fc0ea1f" dependencies = [ "arrow-array", "arrow-buffer", @@ -196,7 +196,7 @@ dependencies = [ "arrow-schema", "arrow-select", "atoi", - "base64", + "base64 0.22.1", "chrono", "comfy-table", "half", @@ -205,22 +205,11 @@ dependencies = [ "ryu", ] -[[package]] -name = "arrow-cmp" -version = "60.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1913e57a64fbf2c4502fa6d54b547d6a1ae59e3146cbc15321d47468c937abd1" -dependencies = [ - "arrow-array", - "arrow-buffer", - "arrow-schema", -] - [[package]] name = "arrow-data" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415b7fbc4cf56b080bb5677a4ae74211b5c79e571ed37a1d547633ec07f3b43b" +checksum = "10791765e6927cdd8ace5bf0e51a69f38a1c3aded2db93561b003c332d02ee2a" dependencies = [ "arrow-buffer", "arrow-schema", @@ -231,13 +220,12 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c58f2ae6dd1480fc413f7320a547e39dcc499c23d4f36ca19572b685811e6e4" +checksum = "908a1b5aeefce5c6eb2a0a4e98f4ccacbd441fe0ed23fb799e634cd0135821e5" dependencies = [ "arrow-array", "arrow-buffer", - "arrow-cmp", "arrow-data", "arrow-schema", "arrow-select", @@ -245,9 +233,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9db8345b0638416d3b56d117d262d13070fdef788c6d75b53a180215c477e" +checksum = "5383b1676492a509032f06fa4ca9522c886036aec4ebe8261bebfbce904af667" dependencies = [ "arrow-array", "arrow-buffer", @@ -258,23 +246,22 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de2c8e714b466404cdc1189e89f892426efea614d938e16c15518c340cd43ba3" +checksum = "8506ad465ebb9483bb6efa1e2e78a313549122c6dd67dc2f7087297b762eaede" dependencies = [ "bitflags", ] [[package]] name = "arrow-select" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5af3f6656c5f2c6944f93d9a357a7f204c93d5f349385a44cf46c4a26fbcf30" +checksum = "fd0f0a0d641356ae2df829af5d635ed9fb93fbeb0d39f3239487cb5fdb37a6ef" dependencies = [ "ahash", "arrow-array", "arrow-buffer", - "arrow-cmp", "arrow-data", "arrow-schema", "num-traits", @@ -282,9 +269,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "60.1.0" +version = "58.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab1be9306b9a413b5b004e2a5a19caac728870d76e8891b351f040d554c93862" +checksum = "bd245a358d52e77bfcb99dc3e21f04b947383271b7a0b9ca8feec43e9039234d" dependencies = [ "arrow-array", "arrow-buffer", @@ -311,9 +298,9 @@ dependencies = [ [[package]] name = "atoi" -version = "3.1.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d12c37257553c19e9aee6cba77814fdc9e07844abfc8b3a2c377cd920ee5a993" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ "num-traits", ] @@ -382,6 +369,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64" version = "0.23.1" @@ -437,8 +430,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -448,6 +439,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + [[package]] name = "chrono" version = "0.4.45" @@ -507,10 +504,11 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "comfy-table" -version = "8.0.1" +version = "7.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96a46ad395a5e00676eddaca4a45ed78b29c08be5a878d35e4b071c625637dd" +checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a" dependencies = [ + "crossterm", "unicode-segmentation", "unicode-width", ] @@ -591,6 +589,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "parking_lot", + "rustix 0.38.44", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -645,12 +665,13 @@ dependencies = [ [[package]] name = "duckdb" -version = "1.20000.1" +version = "1.10506.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d807ca47d4fa206f102ff568046e018fb84518eb0a0b5e65a165a2eb7bd4a7c2" +checksum = "1a46cc5f64f534993bae616668a94b1b8e58295c2bd39f85afef7c44297817da" dependencies = [ "arrow", "cast", + "comfy-table", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -659,6 +680,12 @@ dependencies = [ "strum", ] +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + [[package]] name = "errno" version = "0.3.14" @@ -799,8 +826,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", + "wasm-bindgen", ] [[package]] @@ -957,6 +986,16 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -969,16 +1008,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.107" @@ -1061,19 +1090,18 @@ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libduckdb-sys" -version = "1.20000.1" +version = "1.10506.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0140448c472968ee5cf5ba697435deebe558b366ef1e5c080d24d9696b600725" +checksum = "113d1c26c9f518405a17b5729bb6ab6abf029fdc46626406922cf9a8dc9b4609" dependencies = [ - "cc", "flate2", "pkg-config", "serde", "serde_json", - "sha2 0.11.1", "tar", "ureq", "vcpkg", + "zip", ] [[package]] @@ -1082,12 +1110,27 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.35" @@ -1143,6 +1186,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1154,9 +1209,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.5.1" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ "num-integer", "num-traits", @@ -1208,6 +1263,29 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1262,6 +1340,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.13.1" @@ -1340,6 +1427,19 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.6" @@ -1349,7 +1449,7 @@ dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -1409,6 +1509,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.229" @@ -1631,7 +1737,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.6", "windows-sys 0.61.2", ] @@ -1848,6 +1954,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typed-path" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" + [[package]] name = "typenum" version = "1.20.1" @@ -1890,7 +2002,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde93ff05758a36a851a398a772b0b79a82fbe3c186a46f2696baaeaf84c966b" dependencies = [ - "base64", + "base64 0.23.1", "log", "percent-encoding", "rustls", @@ -1906,7 +2018,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922" dependencies = [ - "base64", + "base64 0.23.1", "http", "httparse", "log", @@ -2020,6 +2132,22 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.11" @@ -2029,6 +2157,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -2183,7 +2317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "963d6fab92d5504b971dc8bd90ca743c2575e590bc8071ea5bc571598667ae08" dependencies = [ "libc", - "rustix", + "rustix 1.1.6", ] [[package]] @@ -2212,6 +2346,23 @@ version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +[[package]] +name = "zip" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f5b58784007ff6e5f280ec82c8596686a3b9b5dd5836c63288d4479f84728b9" +dependencies = [ + "crc32fast", + "flate2", + "getrandom 0.4.3", + "indexmap", + "memchr", + "nix", + "typed-path", + "windows-sys 0.61.2", + "zopfli", +] + [[package]] name = "zlib-rs" version = "0.6.8" @@ -2223,3 +2374,15 @@ name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index d1898b8..36a7a9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" members = ["crates/acaua-core", "crates/acaua-cli"] -exclude = ["apps/gui/src-tauri", "research"] +exclude = ["research"] [workspace.package] version = "0.1.0" @@ -9,7 +9,7 @@ edition = "2021" license = "MIT OR Apache-2.0" repository = "https://git.lucasrguerra.dev.br/ciberfauna/acaua" authors = ["Acauã contributors"] -rust-version = "1.80" +rust-version = "1.88" [profile.release] lto = "thin" diff --git a/Dockerfile b/Dockerfile index 62aa596..85a3a28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,51 @@ # syntax=docker/dockerfile:1 +# +# Acauã server image: the acaua daemon (API and embedded web dashboard) linked +# against the official prebuilt libduckdb. DuckDB is never compiled here. -FROM rust:1-slim-bookworm AS build +# Build stage. +FROM rust:1.99-slim-trixie AS build +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates curl unzip \ + && rm -rf /var/lib/apt/lists/* +COPY scripts/fetch-libduckdb.sh /usr/local/bin/fetch-libduckdb.sh +RUN fetch-libduckdb.sh /opt/duckdb \ + && echo /opt/duckdb > /etc/ld.so.conf.d/duckdb.conf \ + && ldconfig +ENV DUCKDB_LIB_DIR=/opt/duckdb \ + DUCKDB_INCLUDE_DIR=/opt/duckdb 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 +# The ldd check fails the build if the binary is not linked to the shared +# libduckdb (for example if someone re-enables the `bundled` feature). +RUN cargo build --release --locked -p acaua-cli \ + && ldd target/release/acaua | grep -q 'libduckdb.so => /opt/duckdb/libduckdb.so' -FROM debian:bookworm-slim +# Runtime stage: Debian slim, the binary, libduckdb.so, CA certificates and +# curl (update check and HEALTHCHECK). Runs as an unprivileged user. +FROM debian:trixie-slim RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates curl tar \ - && rm -rf /var/lib/apt/lists/* + && apt-get install -y --no-install-recommends ca-certificates curl libstdc++6 \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system --gid 10001 acaua \ + && useradd --system --uid 10001 --gid 10001 --home-dir /data --shell /usr/sbin/nologin acaua \ + && mkdir -p /data/acaua /data/zeek \ + && chown acaua:acaua /data/acaua +COPY --from=build /opt/duckdb/libduckdb.so /opt/duckdb/.duckdb-release /opt/duckdb/ +RUN echo /opt/duckdb > /etc/ld.so.conf.d/duckdb.conf && ldconfig 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.source="https://git.lucasrguerra.dev.br/ciberfauna/acaua" \ org.opencontainers.image.licenses="MIT OR Apache-2.0" +ENV ACAUA_RUNTIME=container +USER acaua WORKDIR /data EXPOSE 8787 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -fsS http://127.0.0.1:8787/api/health >/dev/null || exit 1 ENTRYPOINT ["acaua"] CMD ["serve", "--bind", "0.0.0.0:8787", "--data-dir", "/data/acaua"] diff --git a/README.md b/README.md index 87767fb..07e786a 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,8 @@ not an opaque model. The goal is to answer not only "this looks suspicious" but 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`. +- **Server daemon and web dashboard:** `acaua serve` runs continuous ingestion, + scheduling, the API and the embedded web UI, distributed 100% via Docker. - **Research stays outside the product:** `research/` is an isolated Python workspace for scientific validation and fixtures. It is never compiled, packaged or released. @@ -39,11 +38,12 @@ not an opaque model. The goal is to answer not only "this looks suspicious" but |---|---| | 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 | +| Embedded web dashboard | ✅ UI shell, served by daemon | | Documentation and CI/release workflows | ✅ | +| Docker multi-arch image and compose deployment | ✅ | +| DuckDB analytical store | ✅ official prebuilt library linked | | 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 | @@ -70,20 +70,34 @@ 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. +The daemon binds to `127.0.0.1` by default. The Docker Compose deployment +isolates the daemon in an unprivileged, read-only container with dropped capabilities. ## Install -There are no published releases yet. Build from source: +Run via Docker Compose (recommended for operators): ```bash +cp .env.example .env +docker compose pull && docker compose up -d +``` + +Or build and run locally with Docker: + +```bash +docker compose up -d --build +``` + +To compile from source natively (Rust 1.88+ required): + +```bash +scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb +export DUCKDB_LIB_DIR=~/.cache/acaua/duckdb LD_LIBRARY_PATH=~/.cache/acaua/duckdb 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. +Supported platform matrix for published container images: `linux/amd64` and `linux/arm64`. ## Getting started @@ -95,39 +109,29 @@ 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)**. +does not parse Zeek logs yet. `serve` exposes the stub API and the embedded web UI. +`update` checks for new versions and guides container image updates. ## Development ```bash +scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb +export DUCKDB_LIB_DIR=~/.cache/acaua/duckdb LD_LIBRARY_PATH=~/.cache/acaua/duckdb cargo test --workspace cargo clippy --workspace --all-targets node scripts/check-ui-i18n.mjs -scripts/check.sh +scripts/check.sh --full ``` -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)**. +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. +- There is no published release image 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. +- The project targets Linux container environments (`linux/amd64`, `linux/arm64`). ## Where it's going @@ -135,9 +139,9 @@ Summary of the [ROADMAP](ROADMAP.md): | | | |---|---| -| **Done** | Workspace, CLI/API stubs, GUI shell, documentation, CI/release workflows | +| **Done** | Workspace, CLI/API stubs, web dashboard, Docker containerization, 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 | +| **Later** | Full Parquet partitioning, TTL pruning, pillar 3 and 4, evidence fusion | | **Not planned** | Opaque AI scoring, a required cloud service, Python in the shipped runtime | ## License diff --git a/README.pt-BR.md b/README.pt-BR.md index e6c109e..bb312b1 100644 --- a/README.pt-BR.md +++ b/README.pt-BR.md @@ -27,9 +27,8 @@ produziram esta pontuação". 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. +- **Daemon servidor e dashboard web:** `acaua serve` roda ingestão contínua, + agendador, API e o dashboard web embutido, distribuído 100% via Docker. - **Pesquisa fora do produto:** `research/` é um espaço Python isolado para validação científica e fixtures. Ele nunca é compilado, empacotado ou distribuído. @@ -40,11 +39,12 @@ produziram esta pontuação". |---|---| | 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 | +| Dashboard web embutido | ✅ shell da UI, servido pelo daemon | | Documentação e workflows de CI/release | ✅ | +| Imagem Docker multi-arch e deployment com Compose | ✅ | +| Armazenamento analítico DuckDB | ✅ biblioteca oficial pré-compilada linkada | | 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 | @@ -72,20 +72,34 @@ autorização explícita para monitorar. Capturar ou analisar tráfego de tercei 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. +O daemon escuta em `127.0.0.1` por padrão. A implantação via Docker Compose +isola o daemon em um container não privilegiado, somente leitura e sem capabilities. ## Instalação -Ainda não há releases publicadas. Compile do código: +Executar via Docker Compose (recomendado para operadores): ```bash +cp .env.example .env +docker compose pull && docker compose up -d +``` + +Ou compilar e rodar localmente com Docker: + +```bash +docker compose up -d --build +``` + +Para compilar do código nativamente (requer Rust 1.88+): + +```bash +scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb +export DUCKDB_LIB_DIR=~/.cache/acaua/duckdb LD_LIBRARY_PATH=~/.cache/acaua/duckdb 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. +Matriz de plataformas suportadas para as imagens publicadas: `linux/amd64` e `linux/arm64`. ## Começando @@ -97,39 +111,30 @@ 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)**. +ainda não lê logs Zeek. O `serve` expõe a API stub e o dashboard web embutido. +O `update` checa por versões novas e orienta a atualização da imagem do container. ## Desenvolvimento ```bash +scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb +export DUCKDB_LIB_DIR=~/.cache/acaua/duckdb LD_LIBRARY_PATH=~/.cache/acaua/duckdb cargo test --workspace cargo clippy --workspace --all-targets node scripts/check-ui-i18n.mjs -scripts/check.sh +scripts/check.sh --full ``` -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 +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. +- Não há imagem de release publicada ainda. - 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. +- O projeto mira ambientes de container Linux (`linux/amd64`, `linux/arm64`). ## Para onde vai @@ -137,9 +142,9 @@ Resumo do [ROADMAP](ROADMAP.pt-BR.md): | | | |---|---| -| **Feito** | Workspace, stubs de CLI/API, shell da GUI, documentação, workflows de CI/release | +| **Feito** | Workspace, stubs de CLI/API, dashboard web, containerização Docker, 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 | +| **Depois** | Particionamento Parquet completo, limpeza por TTL, pilares 3 e 4, fusão de evidências | | **Fora de plano** | Pontuação por IA opaca, serviço de nuvem obrigatório, Python no runtime distribuído | ## Licença diff --git a/ROADMAP.md b/ROADMAP.md index fd22531..97b68ea 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,23 +3,22 @@ **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. +**under development**; they represent the core analytical 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. +- CLI commands, Axum API and embedded web dashboard. +- 100% Docker containerization, compose deployment, non-root hardened profile. +- Prebuilt DuckDB linking and Parquet storage foundations. +- Stubs for the four pillars, evidence fusion, and Zeek ingestion. - English/Portuguese documentation, CI/release workflows and `scripts/check.sh`. -- Deterministic smoke tests and a bilingual UI check. +- Deterministic smoke tests and bilingual UI coverage checks. ## 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. + event timestamps, add harmonic resistance, and verify with deterministic fixtures. 2. **Pillar 2 — persistent sessions.** Detect long connections that never produce a short interval, with explicit thresholds and no overlap with pillar 1. @@ -33,9 +32,8 @@ This roadmap separates what exists from what is planned. The four pillars are - 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. +- Analytical store optimizations, daily Parquet partitioning and TTL pruning. +- Scheduled ingestion and automated log rotation handling in daemon mode. - NetFlow/IPFIX as a documented degraded mode and Suricata EVE JSON as an optional secondary input. - Signed release metadata, SBOM and reproducible builds. @@ -46,10 +44,4 @@ This roadmap separates what exists from what is planned. The four pillars are - 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. +- Native desktop shell or binary self-mutation. diff --git a/ROADMAP.pt-BR.md b/ROADMAP.pt-BR.md index 505579a..b27ce2f 100644 --- a/ROADMAP.pt-BR.md +++ b/ROADMAP.pt-BR.md @@ -2,56 +2,44 @@ [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. +Este roadmap separa o que existe do que está planejado. Os quatro pilares estão +**em desenvolvimento**; eles representam o trabalho analítico principal. ## 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. +- Comandos CLI, API Axum e dashboard web embutido. +- Containerização 100% Docker, implantação Compose, perfil endurecido não-root. +- Linkagem da DuckDB pré-compilada e fundações de armazenamento Parquet. +- Stubs dos quatro pilares, fusão de evidências e ingestão Zeek. - 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. +- Testes determinísticos de fumaça e checagem de tradução 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. +1. **Pilar 1 — beaconing.** Implementar o periodograma de Rayleigh/Schuster sobre + timestamps de eventos, resistência a harmônicos e validação com fixtures determinísticas. 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. + produzem intervalos curtos, com limiares explícitos sem sobreposição ao pilar 1. +3. **Parser Zeek real.** Ler `conn.log`, `dns.log`, `ssl.log` e logs de protocolo + com memória delimitada, confinamento de caminhos e ordem determinística. +4. **Registro de evidência.** Emitir registro JSON por alerta contendo 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. + movimentação lateral. +- Pilar 4 — fusão limitada e simétrica com pesos reproduzíveis. +- Otimizações no store analítico, particionamento diário Parquet e limpeza por TTL. +- Ingestão agendada e tratamento automático de rotação de logs em modo daemon. +- NetFlow/IPFIX como modo degradado documentado e Suricata EVE JSON como entrada secundária. +- Metadados assinados de release, 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. +- Pontuação por IA opaca que não consiga explicar um alerta. +- Serviço em 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. +- Tratar registros de fluxo como equivalentes a logs Zeek completos. +- Shell desktop nativo ou auto-modificação de binário. diff --git a/SECURITY.md b/SECURITY.md index 78e3369..ff59869 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,6 +15,9 @@ reproduction when a private channel is available. ## Scope and threat model +- **Container hardening:** the container runs as an unprivileged user (`acaua`, UID 10001), + with a read-only root filesystem (`read_only: true`), all capabilities dropped + (`cap_drop: [ALL]`), and security hardening (`no-new-privileges:true`). - **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 @@ -25,18 +28,18 @@ reproduction when a private channel is available. - **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. +- **Updates:** updates are delivered as verified multi-arch container images. + The daemon terminates gracefully upon receiving `SIGTERM` without data corruption. - **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. +- Vulnerabilities in third-party Zeek, DuckDB 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. +- Detection quality claims: the pillars are defensive heuristics, not absolute guarantees. ## Supported versions diff --git a/SECURITY.pt-BR.md b/SECURITY.pt-BR.md index 1436523..2abb752 100644 --- a/SECURITY.pt-BR.md +++ b/SECURITY.pt-BR.md @@ -2,44 +2,47 @@ [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 +O Acauã é software defensivo, mas processa entradas hostis: capturas de pacotes, +logs Zeek e registros de fluxo podem ser intencionalmente malformados. Trate todo dataset como não confiável. -## Reportando uma vulnerabilidade +## Relatando 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. +Não abra uma issue pública com detalhes de exploração. Abra uma issue mínima +solicitando canal privado ou contate o mantenedor pelo perfil do Gitea listado +no repositório. Inclua a versão afetada, o formato da entrada e uma reprodução +mínima quando o canal privado estiver estabelecido. -## Escopo e modelo de ameaça +## Escopo e modelo de ameaças -- **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 +- **Endurecimento do container:** o container executa com usuário não privilegiado + (`acaua`, UID 10001), filesystem raiz somente leitura (`read_only: true`), todas + as capabilities revogadas (`cap_drop: [ALL]`), e endurecimento de segurança (`no-new-privileges:true`). +- **Parsing de entradas:** uma linha de log malformada não deve causar panic no + daemon, exaurir memória ou escapar do diretório de dados configurado. +- **Manipulação de caminhos:** caminhos de dataset são resolvidos e confinados à + raiz configurada; symlinks externos não são seguidos. +- **Exposição de API:** `serve` escuta em `127.0.0.1` por padrão. Expô-lo em uma + interface de rede é decisão explícita do operador e exige um 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. +- **Integridade da evidência:** a evidência de um alerta deve ser reproduzível a + partir da mesma entrada e versão de código. Nãodeterminismo silencioso é falha de + corretude e segurança. +- **Atualizações:** atualizações são distribuídas como imagens multi-arch de + container verificadas. O daemon finaliza de forma limpa ao receber `SIGTERM` + sem corrupção de dados. +- **Privacidade:** não há telemetria. A retenção padrão é de sete dias para + eventos normalizados; o operador é responsável pela conformidade legal 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. +- Vulnerabilidades em pacotes upstream do Zeek, DuckDB ou do sistema operacional + já corrigidas nas fontes. +- Ataques que dependam de desativar os controles de segurança documentados + (como executar `serve` como root em interface pública). +- Alegações de qualidade de detecção: os pilares são heurísticas defensivas, não garantias absolutas. ## 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. +Apenas a linha `0.1.x` mais recente tem suporte durante a fase pré-alfa. Não há +branch de suporte de longo prazo por enquanto. diff --git a/apps/gui/src-tauri/Cargo.lock b/apps/gui/src-tauri/Cargo.lock deleted file mode 100644 index 1c43552..0000000 --- a/apps/gui/src-tauri/Cargo.lock +++ /dev/null @@ -1,4314 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "acaua-core" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", - "sha2", - "thiserror 1.0.69", - "time", -] - -[[package]] -name = "acaua-gui" -version = "0.1.0" -dependencies = [ - "acaua-core", - "serde_json", - "tauri", - "tauri-build", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" - -[[package]] -name = "alloc-stdlib" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "atk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" -dependencies = [ - "atk-sys", - "glib", - "libc", -] - -[[package]] -name = "atk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" -dependencies = [ - "serde_core", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2", -] - -[[package]] -name = "brotli" -version = "9.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "627b7cfb0578f0eafce087a90fcd40add1231900b69e4d4de2d456260a4f5d81" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34c6783f42c21c952ad7112b354f0ba2f10760e9decea5b1b60523d6e270faaf" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -dependencies = [ - "serde", -] - -[[package]] -name = "cairo-rs" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" -dependencies = [ - "bitflags 2.13.2", - "cairo-sys-rs", - "glib", - "libc", - "once_cell", - "thiserror 1.0.69", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "camino" -version = "1.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" -dependencies = [ - "serde_core", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "cargo_toml" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" -dependencies = [ - "semver", - "serde", - "toml 1.1.8+spec-1.1.0", -] - -[[package]] -name = "cc" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfb" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" -dependencies = [ - "fnv", - "uuid", - "web-time", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "combine" -version = "4.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "cookie" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" -dependencies = [ - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" -dependencies = [ - "bitflags 2.13.2", - "core-foundation", - "core-graphics-types", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.2", - "core-foundation", - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "cssparser" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" -dependencies = [ - "quote", - "syn 3.0.9", -] - -[[package]] -name = "ctor" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" - -[[package]] -name = "darling" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 3.0.9", -] - -[[package]] -name = "darling_macro" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" -dependencies = [ - "darling_core", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "dbus" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "defmt" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" -dependencies = [ - "bitflags 1.3.2", - "defmt-macros", -] - -[[package]] -name = "defmt-macros" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" -dependencies = [ - "defmt-parser", - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "defmt-parser" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" -dependencies = [ - "thiserror 2.0.21", -] - -[[package]] -name = "deranged" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" -dependencies = [ - "serde_core", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "syn 2.0.119", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "dirs" -version = "7.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users", - "windows-sys 0.61.2", -] - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.2", - "block2", - "libc", - "objc2", -] - -[[package]] -name = "displaydoc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "dom_query" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" -dependencies = [ - "bit-set", - "cssparser", - "foldhash", - "html5ever", - "precomputed-hash", - "selectors", - "tendril", -] - -[[package]] -name = "dpi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -dependencies = [ - "serde", -] - -[[package]] -name = "dtoa" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "embed-resource" -version = "3.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" -dependencies = [ - "cc", - "memchr", - "rustc_version", - "toml 1.1.8+spec-1.1.0", - "vswhom", - "winreg", -] - -[[package]] -name = "embed_plist" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" - -[[package]] -name = "equivalent" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" - -[[package]] -name = "erased-serde" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66" -dependencies = [ - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "fdeflate" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" - -[[package]] -name = "flate2" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" -dependencies = [ - "crc32fast", - "miniz_oxide 0.9.1", - "zlib-rs", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-executor" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gdk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk-sys", - "gio", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", - "once_cell", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkwayland-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" -dependencies = [ - "gdk-sys", - "glib-sys", - "gobject-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gdkx11" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" -dependencies = [ - "gdk", - "gdkx11-sys", - "gio", - "glib", - "libc", - "x11", -] - -[[package]] -name = "gdkx11-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" -dependencies = [ - "gdk-sys", - "glib-sys", - "libc", - "system-deps", - "x11", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "gio" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "once_cell", - "pin-project-lite", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "gio-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "winapi", -] - -[[package]] -name = "glib" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" -dependencies = [ - "bitflags 2.13.2", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror 1.0.69", -] - -[[package]] -name = "glib-macros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" -dependencies = [ - "heck 0.4.1", - "proc-macro-crate 2.0.2", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "glib-sys" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "glob" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" - -[[package]] -name = "gobject-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" -dependencies = [ - "atk", - "cairo-rs", - "field-offset", - "futures-channel", - "gdk", - "gdk-pixbuf", - "gio", - "glib", - "gtk-sys", - "gtk3-macros", - "libc", - "pango", - "pkg-config", -] - -[[package]] -name = "gtk-sys" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" -dependencies = [ - "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk3-macros" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" -dependencies = [ - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "html5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" -dependencies = [ - "log", - "markup5ever", -] - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" -dependencies = [ - "atomic-waker", - "bytes", - "futures-core", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-util" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" -dependencies = [ - "base64 0.23.1", - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "httparse", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ico" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167" -dependencies = [ - "byteorder", - "png", -] - -[[package]] -name = "icu_collections" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" - -[[package]] -name = "icu_properties" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" - -[[package]] -name = "icu_provider" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "infer" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" -dependencies = [ - "cfb", -] - -[[package]] -name = "ipnet" -version = "2.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "javascriptcore-rs" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" -dependencies = [ - "bitflags 1.3.2", - "glib", - "javascriptcore-rs-sys", -] - -[[package]] -name = "javascriptcore-rs-sys" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "jiff" -version = "0.2.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" -dependencies = [ - "defmt", - "jiff-core", - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-link", -] - -[[package]] -name = "jiff-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" -dependencies = [ - "defmt", - "log", -] - -[[package]] -name = "jiff-static" -version = "0.2.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" -dependencies = [ - "jiff-core", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys 0.3.1", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "js-sys" -version = "0.3.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "json-patch" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" -dependencies = [ - "jsonptr", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "jsonptr" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "keyboard-types" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" -dependencies = [ - "bitflags 2.13.2", - "serde", -] - -[[package]] -name = "libappindicator" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" -dependencies = [ - "glib", - "gtk", - "gtk-sys", - "libappindicator-sys", - "log", -] - -[[package]] -name = "libappindicator-sys" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" -dependencies = [ - "gtk-sys", - "libloading", - "once_cell", -] - -[[package]] -name = "libc" -version = "0.2.191" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" - -[[package]] -name = "libdbus-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libredox" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" -dependencies = [ - "libc", -] - -[[package]] -name = "litemap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" - -[[package]] -name = "markup5ever" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "miniz_oxide" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "muda" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" -dependencies = [ - "crossbeam-channel", - "dpi", - "gtk", - "keyboard-types", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.21", - "windows-sys 0.61.2", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.2", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate 3.5.0", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "objc2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" -dependencies = [ - "objc2-encode", - "objc2-exception-helper", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.2", - "block2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", - "objc2-quartz-core", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.2", - "dispatch2", - "objc2", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.2", - "dispatch2", - "objc2", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-location" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-exception-helper" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" -dependencies = [ - "cc", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.2", - "block2", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.2", - "objc2", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" -dependencies = [ - "bitflags 2.13.2", - "block2", - "objc2", - "objc2-cloud-kit", - "objc2-core-data", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-core-location", - "objc2-core-text", - "objc2-foundation", - "objc2-quartz-core", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" -dependencies = [ - "objc2", - "objc2-foundation", -] - -[[package]] -name = "objc2-web-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" -dependencies = [ - "bitflags 2.13.2", - "block2", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "pango" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" -dependencies = [ - "gio", - "glib", - "libc", - "once_cell", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - -[[package]] -name = "plist" -version = "1.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" -dependencies = [ - "base64 0.23.1", - "indexmap 2.14.2", - "quick-xml", - "serde", - "time", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" -dependencies = [ - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.17+spec-1.1.0", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quick-xml" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.2", -] - -[[package]] -name = "redox_users" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" -dependencies = [ - "libredox", - "thiserror 2.0.21", -] - -[[package]] -name = "ref-cast" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - -[[package]] -name = "reqwest" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" -dependencies = [ - "base64 0.23.1", - "bytes", - "futures-core", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemars" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" -dependencies = [ - "dyn-clone", - "indexmap 1.9.3", - "schemars_derive", - "serde", - "serde_json", - "url", - "uuid", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars_derive" -version = "0.8.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.119", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "selectors" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" -dependencies = [ - "bitflags 2.13.2", - "cssparser", - "derive_more", - "log", - "new_debug_unreachable", - "phf", - "phf_codegen", - "precomputed-hash", - "rustc-hash", - "servo_arc", - "smallvec", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-untagged" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" -dependencies = [ - "erased-serde", - "serde", - "serde_core", - "typeid", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "serde_json" -version = "1.0.154" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_with" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" -dependencies = [ - "base64 0.23.1", - "bs58", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.2", - "jiff", - "schemars 0.9.0", - "schemars 1.2.3", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "serialize-to-javascript" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" -dependencies = [ - "serde", - "serde_json", - "serialize-to-javascript-impl", -] - -[[package]] -name = "serialize-to-javascript-impl" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "servo_arc" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" -dependencies = [ - "stable_deref_trait", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[package]] -name = "siphasher" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" - -[[package]] -name = "socket2" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" -dependencies = [ - "bytemuck", - "js-sys", - "ndk", - "objc2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "redox_syscall", - "tracing", - "wasm-bindgen", - "web-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "soup3" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" -dependencies = [ - "futures-channel", - "gio", - "glib", - "libc", - "soup3-sys", -] - -[[package]] -name = "soup3-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_vcruntime" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7" - -[[package]] -name = "string_cache" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "string_cache_codegen" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "swift-rs" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.2", - "version-compare", -] - -[[package]] -name = "tao" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" -dependencies = [ - "bitflags 2.13.2", - "block2", - "core-foundation", - "core-graphics", - "crossbeam-channel", - "dbus", - "dispatch2", - "dlopen2", - "dpi", - "gdkwayland-sys", - "gdkx11-sys", - "gtk", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "parking_lot", - "percent-encoding", - "raw-window-handle", - "tao-macros", - "unicode-segmentation", - "url", - "windows", - "windows-core", - "windows-version", - "x11-dl", -] - -[[package]] -name = "tao-macros" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - -[[package]] -name = "tauri" -version = "2.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072" -dependencies = [ - "anyhow", - "bytes", - "cookie", - "dirs", - "dunce", - "embed_plist", - "getrandom 0.3.4", - "glob", - "gtk", - "heck 0.5.0", - "http", - "jni", - "libc", - "log", - "mime", - "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "plist", - "raw-window-handle", - "reqwest", - "serde", - "serde_json", - "serde_repr", - "serialize-to-javascript", - "swift-rs", - "tauri-build", - "tauri-macros", - "tauri-runtime", - "tauri-runtime-wry", - "tauri-utils", - "thiserror 2.0.21", - "tokio", - "tray-icon", - "url", - "webkit2gtk", - "webview2-com", - "window-vibrancy", - "windows", -] - -[[package]] -name = "tauri-build" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614" -dependencies = [ - "anyhow", - "cargo_toml", - "dirs", - "glob", - "heck 0.5.0", - "json-patch", - "schemars 0.8.22", - "semver", - "serde", - "serde_json", - "static_vcruntime", - "strsim", - "tauri-utils", - "tauri-winres", - "walkdir", -] - -[[package]] -name = "tauri-codegen" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316" -dependencies = [ - "base64 0.23.1", - "brotli", - "ico", - "json-patch", - "plist", - "png", - "proc-macro2", - "quote", - "semver", - "serde", - "serde_json", - "sha2", - "syn 2.0.119", - "tauri-utils", - "thiserror 2.0.21", - "time", - "url", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-macros" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.119", - "tauri-codegen", - "tauri-utils", -] - -[[package]] -name = "tauri-runtime" -version = "2.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9" -dependencies = [ - "cookie", - "dpi", - "gtk", - "http", - "jni", - "objc2", - "objc2-ui-kit", - "objc2-web-kit", - "raw-window-handle", - "serde", - "serde_json", - "tauri-utils", - "thiserror 2.0.21", - "url", - "webkit2gtk", - "webview2-com", - "windows", -] - -[[package]] -name = "tauri-runtime-wry" -version = "2.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" -dependencies = [ - "gtk", - "http", - "jni", - "log", - "objc2", - "objc2-app-kit", - "percent-encoding", - "raw-window-handle", - "softbuffer", - "tao", - "tauri-runtime", - "tauri-utils", - "url", - "webkit2gtk", - "webview2-com", - "windows", - "wry", -] - -[[package]] -name = "tauri-utils" -version = "2.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3" -dependencies = [ - "anyhow", - "brotli", - "cargo_metadata", - "ctor", - "dom_query", - "dunce", - "glob", - "http", - "infer", - "json-patch", - "log", - "memchr", - "phf", - "plist", - "proc-macro2", - "quote", - "regex", - "schemars 0.8.22", - "semver", - "serde", - "serde-untagged", - "serde_json", - "serde_with", - "swift-rs", - "thiserror 2.0.21", - "toml 1.1.8+spec-1.1.0", - "url", - "urlpattern", - "uuid", - "walkdir", -] - -[[package]] -name = "tauri-winres" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52bb6904475a6ca662f15ef787f18ade61d5740a1ee12c45d3b748efdb6f796" -dependencies = [ - "dunce", - "embed-resource", - "toml 1.1.8+spec-1.1.0", -] - -[[package]] -name = "tendril" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" -dependencies = [ - "new_debug_unreachable", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl 2.0.21", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "time" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" - -[[package]] -name = "time-macros" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" - -[[package]] -name = "tokio" -version = "1.54.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-util" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "toml_edit 0.20.2", -] - -[[package]] -name = "toml" -version = "1.1.8+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" -dependencies = [ - "indexmap 2.14.2", - "serde_core", - "serde_spanned 1.1.2", - "toml_datetime 1.1.2+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.4", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.14.2", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" -dependencies = [ - "indexmap 2.14.2", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.3", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.25.17+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" -dependencies = [ - "indexmap 2.14.2", - "toml_datetime 1.1.2+spec-1.1.0", - "toml_parser", - "winnow 1.0.4", -] - -[[package]] -name = "toml_parser" -version = "1.1.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" -dependencies = [ - "winnow 1.0.4", -] - -[[package]] -name = "toml_writer" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" -dependencies = [ - "bitflags 2.13.2", - "bytes", - "futures-util", - "http", - "http-body", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", - "url", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tray-icon" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" -dependencies = [ - "crossbeam-channel", - "dirs", - "libappindicator", - "muda", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation", - "once_cell", - "png", - "serde", - "thiserror 2.0.21", - "windows-sys 0.61.2", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-ident" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", - "serde_derive", -] - -[[package]] -name = "urlpattern" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" -dependencies = [ - "icu_properties", - "regex", - "serde", - "url", -] - -[[package]] -name = "utf8_iter" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" -dependencies = [ - "utf_types", -] - -[[package]] -name = "utf_types" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" - -[[package]] -name = "uuid" -version = "1.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" -dependencies = [ - "cfg-if", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" -dependencies = [ - "js-sys", - "tokio", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 3.0.9", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.130" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-streams" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "web-sys" -version = "0.3.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web_atoms" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" -dependencies = [ - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "webkit2gtk" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" -dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "gdk", - "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", - "gtk", - "gtk-sys", - "javascriptcore-rs", - "libc", - "once_cell", - "soup3", - "webkit2gtk-sys", -] - -[[package]] -name = "webkit2gtk-sys" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" -dependencies = [ - "bitflags 1.3.2", - "cairo-sys-rs", - "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "gtk-sys", - "javascriptcore-rs-sys", - "libc", - "pkg-config", - "soup3-sys", - "system-deps", -] - -[[package]] -name = "webview2-com" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" -dependencies = [ - "webview2-com-macros", - "webview2-com-sys", - "windows", - "windows-core", -] - -[[package]] -name = "webview2-com-macros" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "webview2-com-sys" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" -dependencies = [ - "thiserror 2.0.21", - "windows", - "windows-core", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "window-vibrancy" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" -dependencies = [ - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-quartz-core", - "raw-window-handle", - "windows-sys 0.61.2", - "windows-version", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-version" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "winnow" -version = "0.5.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "winreg" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" -dependencies = [ - "cfg-if", - "windows-sys 0.61.2", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "writeable" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" - -[[package]] -name = "wry" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" -dependencies = [ - "base64 0.23.1", - "block2", - "cookie", - "crossbeam-channel", - "dirs", - "dom_query", - "dpi", - "dunce", - "gdkx11", - "gtk", - "http", - "javascriptcore-rs", - "jni", - "libc", - "ndk", - "objc2", - "objc2-app-kit", - "objc2-core-foundation", - "objc2-foundation", - "objc2-ui-kit", - "objc2-web-kit", - "percent-encoding", - "raw-window-handle", - "sha2", - "soup3", - "tao-macros", - "thiserror 2.0.21", - "url", - "webkit2gtk", - "webkit2gtk-sys", - "webview2-com", - "windows", - "windows-core", - "windows-version", - "x11-dl", -] - -[[package]] -name = "x11" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "yoke" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", - "synstructure", -] - -[[package]] -name = "zerofrom" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "zlib-rs" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/gui/src-tauri/Cargo.toml b/apps/gui/src-tauri/Cargo.toml deleted file mode 100644 index 6d033aa..0000000 --- a/apps/gui/src-tauri/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[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 diff --git a/apps/gui/src-tauri/build.rs b/apps/gui/src-tauri/build.rs deleted file mode 100644 index e52c9aa..0000000 --- a/apps/gui/src-tauri/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -fn main() { - println!("cargo:rerun-if-changed=../../../logo.png"); - let _ = std::fs::copy("../../../logo.png", "../ui/logo.png"); - tauri_build::build(); -} diff --git a/apps/gui/src-tauri/capabilities/default.json b/apps/gui/src-tauri/capabilities/default.json deleted file mode 100644 index b5e8009..0000000 --- a/apps/gui/src-tauri/capabilities/default.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "$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"] -} diff --git a/apps/gui/src-tauri/icons/128x128.png b/apps/gui/src-tauri/icons/128x128.png deleted file mode 100644 index 17b4494..0000000 Binary files a/apps/gui/src-tauri/icons/128x128.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/128x128@2x.png b/apps/gui/src-tauri/icons/128x128@2x.png deleted file mode 100644 index d901c87..0000000 Binary files a/apps/gui/src-tauri/icons/128x128@2x.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/32x32.png b/apps/gui/src-tauri/icons/32x32.png deleted file mode 100644 index b013138..0000000 Binary files a/apps/gui/src-tauri/icons/32x32.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/64x64.png b/apps/gui/src-tauri/icons/64x64.png deleted file mode 100644 index 6a628e4..0000000 Binary files a/apps/gui/src-tauri/icons/64x64.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square107x107Logo.png b/apps/gui/src-tauri/icons/Square107x107Logo.png deleted file mode 100644 index 588f371..0000000 Binary files a/apps/gui/src-tauri/icons/Square107x107Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square142x142Logo.png b/apps/gui/src-tauri/icons/Square142x142Logo.png deleted file mode 100644 index 39e9f81..0000000 Binary files a/apps/gui/src-tauri/icons/Square142x142Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square150x150Logo.png b/apps/gui/src-tauri/icons/Square150x150Logo.png deleted file mode 100644 index 34ec414..0000000 Binary files a/apps/gui/src-tauri/icons/Square150x150Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square284x284Logo.png b/apps/gui/src-tauri/icons/Square284x284Logo.png deleted file mode 100644 index 72c368b..0000000 Binary files a/apps/gui/src-tauri/icons/Square284x284Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square30x30Logo.png b/apps/gui/src-tauri/icons/Square30x30Logo.png deleted file mode 100644 index 6e0c87b..0000000 Binary files a/apps/gui/src-tauri/icons/Square30x30Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square310x310Logo.png b/apps/gui/src-tauri/icons/Square310x310Logo.png deleted file mode 100644 index 125d2b3..0000000 Binary files a/apps/gui/src-tauri/icons/Square310x310Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square44x44Logo.png b/apps/gui/src-tauri/icons/Square44x44Logo.png deleted file mode 100644 index f50b4f7..0000000 Binary files a/apps/gui/src-tauri/icons/Square44x44Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square71x71Logo.png b/apps/gui/src-tauri/icons/Square71x71Logo.png deleted file mode 100644 index 3fbf446..0000000 Binary files a/apps/gui/src-tauri/icons/Square71x71Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/Square89x89Logo.png b/apps/gui/src-tauri/icons/Square89x89Logo.png deleted file mode 100644 index 7668aad..0000000 Binary files a/apps/gui/src-tauri/icons/Square89x89Logo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/StoreLogo.png b/apps/gui/src-tauri/icons/StoreLogo.png deleted file mode 100644 index 3388197..0000000 Binary files a/apps/gui/src-tauri/icons/StoreLogo.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/icon.icns b/apps/gui/src-tauri/icons/icon.icns deleted file mode 100644 index 715f1a1..0000000 Binary files a/apps/gui/src-tauri/icons/icon.icns and /dev/null differ diff --git a/apps/gui/src-tauri/icons/icon.ico b/apps/gui/src-tauri/icons/icon.ico deleted file mode 100644 index 977342f..0000000 Binary files a/apps/gui/src-tauri/icons/icon.ico and /dev/null differ diff --git a/apps/gui/src-tauri/icons/icon.png b/apps/gui/src-tauri/icons/icon.png deleted file mode 100644 index ad4919c..0000000 Binary files a/apps/gui/src-tauri/icons/icon.png and /dev/null differ diff --git a/apps/gui/src-tauri/icons/source.png b/apps/gui/src-tauri/icons/source.png deleted file mode 100644 index 78fea1f..0000000 Binary files a/apps/gui/src-tauri/icons/source.png and /dev/null differ diff --git a/apps/gui/src-tauri/src/main.rs b/apps/gui/src-tauri/src/main.rs deleted file mode 100644 index 318fcc7..0000000 --- a/apps/gui/src-tauri/src/main.rs +++ /dev/null @@ -1,62 +0,0 @@ -#![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::to_value(doctor::run()).map_err(|error| error.to_string()) -} - -#[tauri::command] -fn analyze_dataset(path: String) -> Result { - 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 { - 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 { - 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ã"); -} diff --git a/apps/gui/src-tauri/tauri.conf.json b/apps/gui/src-tauri/tauri.conf.json deleted file mode 100644 index 16282c9..0000000 --- a/apps/gui/src-tauri/tauri.conf.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$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" - ] - } -} diff --git a/apps/gui/ui/app.js b/apps/gui/ui/app.js index ccae50b..3b2ff2b 100644 --- a/apps/gui/ui/app.js +++ b/apps/gui/ui/app.js @@ -1,7 +1,6 @@ import { setLang, tr } from './i18n.js'; const $ = (selector) => document.querySelector(selector); -const tauriInvoke = window.__TAURI__?.core?.invoke; const connection = $('#connection'); const connectionText = $('#connectionText'); @@ -30,7 +29,6 @@ async function fetchJson(path, options = {}) { } async function call(command, args = {}) { - if (tauriInvoke) return tauriInvoke(command, args); switch (command) { case 'app_info': return fetchJson('/api/status'); diff --git a/crates/acaua-cli/src/main.rs b/crates/acaua-cli/src/main.rs index 44b17b2..bfeeeb7 100644 --- a/crates/acaua-cli/src/main.rs +++ b/crates/acaua-cli/src/main.rs @@ -25,6 +25,8 @@ enum Command { }, Analyze { directory: PathBuf, + #[arg(long, default_value = ".acaua")] + data_dir: PathBuf, #[arg(long)] json: bool, }, @@ -32,10 +34,7 @@ enum Command { #[arg(long)] json: bool, }, - Update { - #[arg(long)] - apply: bool, - }, + Update, } #[tokio::main] @@ -50,8 +49,12 @@ async fn main() { 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)?; + Command::Analyze { + directory, + data_dir, + json, + } => { + let report = zeek::analyze_dir(&directory, &data_dir)?; if json { println!("{}", serde_json::to_string_pretty(&report)?); } else { @@ -59,6 +62,9 @@ async fn run(cli: Cli) -> acaua_core::Result<()> { println!("logs lidos: {}", report.logs_seen); println!("registros: {}", report.records); println!("alertas: {}", report.alerts.len()); + for alert in &report.alerts { + println!(" [{:.3}] {}", alert.score, alert.summary); + } println!("estado: {}", report.status); } Ok(()) @@ -80,26 +86,27 @@ async fn run(cli: Cli) -> acaua_core::Result<()> { } 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" - } - ); + Command::Update => { + 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" + } + ); + if status.update_available { + if std::env::var("ACAUA_RUNTIME").as_deref() == Ok("container") { + println!("para atualizar: docker compose pull && docker compose up -d"); + } else { + println!("para atualizar: baixe a nova imagem ou recompile a partir da tag"); + } } Ok(()) } diff --git a/crates/acaua-cli/src/web.rs b/crates/acaua-cli/src/web.rs index 997b6a3..28f22e0 100644 --- a/crates/acaua-cli/src/web.rs +++ b/crates/acaua-cli/src/web.rs @@ -75,16 +75,19 @@ async fn alerts() -> Json> { } async fn analyze( + State(state): State, Json(request): Json, ) -> std::result::Result, (StatusCode, Json)> { - zeek::analyze_dir(&request.path).map(Json).map_err(|error| { - ( - StatusCode::BAD_REQUEST, - Json(ErrorBody { - error: error.to_string(), - }), - ) - }) + zeek::analyze_dir(&request.path, state.storage.root()) + .map(Json) + .map_err(|error| { + ( + StatusCode::BAD_REQUEST, + Json(ErrorBody { + error: error.to_string(), + }), + ) + }) } async fn update_check() -> std::result::Result, (StatusCode, Json)> { @@ -100,15 +103,6 @@ async fn update_check() -> std::result::Result, (StatusCode, }) } -async fn update_apply() -> (StatusCode, Json) { - ( - 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)) } @@ -129,21 +123,38 @@ async fn asset(uri: Uri) -> Response { } async fn shutdown_signal() { - let _ = tokio::signal::ctrl_c().await; + let interrupt = async { + let _ = tokio::signal::ctrl_c().await; + }; + #[cfg(unix)] + let terminate = async { + match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { + Ok(mut stream) => { + stream.recv().await; + } + Err(_) => std::future::pending::<()>().await, + } + }; + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + tokio::select! { + _ = interrupt => {} + _ = terminate => {} + } } 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)), + 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("/api/update", get(update_check)) .route("/logo.png", get(logo)) .fallback(asset) .with_state(state) diff --git a/crates/acaua-core/Cargo.toml b/crates/acaua-core/Cargo.toml index c8d29c5..0c39b1e 100644 --- a/crates/acaua-core/Cargo.toml +++ b/crates/acaua-core/Cargo.toml @@ -8,17 +8,17 @@ 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 } +chrono = { version = "0.4", default-features = false, features = ["std"] } +# Linked against the official prebuilt libduckdb (scripts/fetch-libduckdb.sh): +# never enable `bundled`, `parquet`, `json` or `extensions-full`, which compile +# DuckDB from source. Parquet, JSON and ICU are already inside libduckdb.so. +duckdb = "~1.10506.1" [dev-dependencies] tempfile = "3" diff --git a/crates/acaua-core/src/doctor.rs b/crates/acaua-core/src/doctor.rs index 3ac2e95..f0b7008 100644 --- a/crates/acaua-core/src/doctor.rs +++ b/crates/acaua-core/src/doctor.rs @@ -14,40 +14,79 @@ pub struct DoctorReport { 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" }, +fn zeek_check() -> Check { + if std::env::var("ACAUA_RUNTIME").as_deref() == Ok("container") { + Check { + name: "zeek", ok: true, - detail: format!("{name} is available"), + detail: "zeek runs in a dedicated container".to_string(), + } + } else { + match Command::new("zeek").arg("--version").output() { + Ok(output) if output.status.success() => Check { + name: "zeek", + ok: true, + detail: "zeek is available".to_string(), + }, + Ok(_) => Check { + name: "zeek", + ok: false, + detail: "zeek returned an error".to_string(), + }, + Err(error) => Check { + name: "zeek", + ok: false, + detail: format!("zeek not found: {error}"), + }, + } + } +} + +fn curl_check() -> Check { + match Command::new("curl").arg("--version").output() { + Ok(output) if output.status.success() => Check { + name: "curl", + ok: true, + detail: "curl is available".to_string(), }, Ok(_) => Check { - name: if name == "zeek" { "zeek" } else { "runtime" }, + name: "curl", ok: false, - detail: format!("{name} returned an error"), + detail: "curl returned an error".to_string(), }, Err(error) => Check { - name: if name == "zeek" { "zeek" } else { "runtime" }, + name: "curl", ok: false, - detail: format!("{name} not found: {error}"), + detail: format!("curl not found: {error}"), + }, + } +} + +fn duckdb_check() -> Check { + let result = duckdb::Connection::open_in_memory().and_then(|conn| { + conn.query_row("SELECT library_version FROM pragma_version()", [], |row| { + row.get::<_, String>(0) + }) + }); + match result { + Ok(version) => Check { + name: "duckdb", + ok: true, + detail: format!("libduckdb {version} linked"), + }, + Err(error) => Check { + name: "duckdb", + ok: false, + detail: error.to_string(), }, } } 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() - }, - }, + zeek_check(), + curl_check(), + duckdb_check(), Check { name: "research", ok: true, diff --git a/crates/acaua-core/src/error.rs b/crates/acaua-core/src/error.rs index e8ff7cf..57e68fa 100644 --- a/crates/acaua-core/src/error.rs +++ b/crates/acaua-core/src/error.rs @@ -16,6 +16,9 @@ pub enum AcauaError { #[error("serialization error: {0}")] Serde(#[from] serde_json::Error), + + #[error("database error: {0}")] + Database(#[from] duckdb::Error), } pub type Result = std::result::Result; diff --git a/crates/acaua-core/src/pillar1.rs b/crates/acaua-core/src/pillar1.rs index 293b4a7..82bf942 100644 --- a/crates/acaua-core/src/pillar1.rs +++ b/crates/acaua-core/src/pillar1.rs @@ -6,19 +6,370 @@ pub struct BeaconingInput { pub timestamps: Vec, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct GridInfo { + pub min_period_s: f64, + pub max_period_s: f64, + pub points: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct WeightsInfo { + pub significance: f64, + pub regularity: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct BeaconingScore { pub score: f64, pub observations: usize, + pub period_detected_seconds: f64, + pub p_value: f64, + pub peak_power: f64, + pub madm_normalized: f64, + pub skew_bowley: f64, + pub degenerate_quartiles: bool, pub method: &'static str, + pub grid: GridInfo, + pub weights: WeightsInfo, + pub formula_version: &'static str, pub status: &'static str, } +fn quantile_cont(sorted: &[f64], q: f64) -> f64 { + if sorted.is_empty() { + return 0.0; + } + if sorted.len() == 1 { + return sorted[0]; + } + let idx = (sorted.len() - 1) as f64 * q; + let lower = idx.floor() as usize; + let upper = idx.ceil() as usize; + let frac = idx - lower as f64; + if lower == upper { + sorted[lower] + } else { + sorted[lower] + frac * (sorted[upper] - sorted[lower]) + } +} + pub fn score(input: &BeaconingInput) -> Result { + let grid_info = GridInfo { + min_period_s: 600.0, + max_period_s: 43200.0, + points: 200, + }; + let weights_info = WeightsInfo { + significance: 0.5, + regularity: 0.5, + }; + + let n = input.timestamps.len(); + if n < 20 { + return Ok(BeaconingScore { + score: 0.0, + observations: n, + period_detected_seconds: 0.0, + p_value: 1.0, + peak_power: 0.0, + madm_normalized: 0.0, + skew_bowley: 0.0, + degenerate_quartiles: false, + method: "rayleigh-schuster", + grid: grid_info, + weights: weights_info, + formula_version: "1.1", + status: "insufficient_data", + }); + } + + let mut ts = input.timestamps.clone(); + ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + + let mut deltas = Vec::with_capacity(n - 1); + for i in 1..n { + deltas.push(ts[i] - ts[i - 1]); + } + + let mut sorted_deltas = deltas.clone(); + sorted_deltas.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + + let q1 = quantile_cont(&sorted_deltas, 0.25); + let q2 = quantile_cont(&sorted_deltas, 0.50); + let q3 = quantile_cont(&sorted_deltas, 0.75); + + const EPSILON: f64 = 1e-9; + let (skew_bowley, degenerate_quartiles) = if (q3 - q1).abs() < EPSILON { + (0.0, true) + } else { + ((q3 + q1 - 2.0 * q2) / (q3 - q1), false) + }; + + let mut abs_devs: Vec = deltas.iter().map(|&d| (d - q2).abs()).collect(); + abs_devs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let madm = quantile_cont(&abs_devs, 0.50); + + let (madm_normalized, degenerate_deltas) = if q2 <= EPSILON { + (0.0, true) + } else { + (madm / q2, false) + }; + + // Rayleigh/Schuster periodogram with M = 200 log-spaced periods from 600s to 43200s + let t0 = ts[0]; + let num_periods = 200; + let mut periods = Vec::with_capacity(num_periods); + let mut powers = Vec::with_capacity(num_periods); + + for j in 0..num_periods { + let period = 600.0 * 72.0_f64.powf(j as f64 / 199.0); + periods.push(period); + let f = 1.0 / period; + let mut sum_cos = 0.0; + let mut sum_sin = 0.0; + for &t in &ts { + let phase = 2.0 * std::f64::consts::PI * f * (t - t0); + sum_cos += phase.cos(); + sum_sin += phase.sin(); + } + let power = (sum_cos * sum_cos + sum_sin * sum_sin) / (n as f64); + powers.push(power); + } + + let mut p_max = 0.0_f64; + for &p in &powers { + if p > p_max { + p_max = p; + } + } + + // Harmonic resistance: largest period in the grid with P >= 0.5 * P_max + let threshold_power = 0.5 * p_max; + let mut detected_period = periods[0]; + for j in (0..num_periods).rev() { + if powers[j] >= threshold_power { + detected_period = periods[j]; + break; + } + } + + let m_points = num_periods as f64; + let p_value = (m_points * (-p_max).exp()).min(1.0); + + // Formula v1.1 + let ln_m = m_points.ln(); + let six_ln_10 = 6.0 * 10.0_f64.ln(); + let sig = ((p_max - ln_m) / six_ln_10).clamp(0.0, 1.0); + let regularity = (1.0 - 2.0 * madm_normalized).clamp(0.0, 1.0); + + let status = if degenerate_deltas { + "degenerate_deltas" + } else { + "ok" + }; + + let score = if degenerate_deltas { + 0.5 * sig + } else { + 0.5 * sig + 0.5 * regularity + }; + Ok(BeaconingScore { - score: 0.0, - observations: input.timestamps.len(), - method: "rayleigh-schuster-stub", - status: "skeleton", + score, + observations: n, + period_detected_seconds: detected_period, + p_value, + peak_power: p_max, + madm_normalized, + skew_bowley, + degenerate_quartiles, + method: "rayleigh-schuster", + grid: grid_info, + weights: weights_info, + formula_version: "1.1", + status, }) } + +#[cfg(test)] +mod tests { + use super::*; + + struct SplitMix64 { + state: u64, + } + + impl SplitMix64 { + fn new(seed: u64) -> Self { + Self { state: seed } + } + + fn next_u64(&mut self) -> u64 { + self.state = self.state.wrapping_add(0x9e3779b97f4a7c15); + let mut z = self.state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z ^ (z >> 31) + } + + fn next_f64(&mut self) -> f64 { + (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64 + } + + fn uniform(&mut self, low: f64, high: f64) -> f64 { + low + (high - low) * self.next_f64() + } + + fn exponential(&mut self, mean: f64) -> f64 { + let u = self.next_f64().max(1e-15); + -mean * u.ln() + } + } + + #[test] + fn test_1_perfectly_periodic_series() { + let mut timestamps = Vec::with_capacity(42); + for k in 0..42 { + timestamps.push(k as f64 * 14400.0); + } + let res = score(&BeaconingInput { timestamps }).unwrap(); + assert!(res.score >= 0.99, "score: {}", res.score); + let rel_err = (res.period_detected_seconds - 14400.0).abs() / 14400.0; + assert!( + rel_err < 0.02, + "period: {}, rel_err: {}", + res.period_detected_seconds, + rel_err + ); + assert_eq!(res.status, "ok"); + } + + #[test] + fn test_2_grid_jitter() { + let mut min_score = f64::MAX; + let mut max_score = f64::MIN; + for seed in 1..=50 { + let mut rng = SplitMix64::new(seed); + let t0 = 1000.0; + let mut timestamps = Vec::with_capacity(42); + for k in 0..42 { + let jitter = rng.uniform(-0.18, 0.18) * 14400.0; + timestamps.push(t0 + k as f64 * 14400.0 + jitter); + } + let res = score(&BeaconingInput { timestamps }).unwrap(); + assert!(res.score >= 0.80, "seed {seed} score {} < 0.80", res.score); + if res.score < min_score { + min_score = res.score; + } + if res.score > max_score { + max_score = res.score; + } + } + println!("Test 2 (grid jitter) 50 seeds: min={min_score:.4}, max={max_score:.4}"); + } + + #[test] + fn test_3_cumulative_jitter() { + let mut min_score = f64::MAX; + let mut max_score = f64::MIN; + let mut period_within_10pct = 0; + + for seed in 1..=50 { + let mut rng = SplitMix64::new(seed); + let mut timestamps = Vec::with_capacity(42); + let mut curr = 1000.0; + timestamps.push(curr); + for _ in 1..42 { + curr += 14400.0 * (1.0 + rng.uniform(-0.18, 0.18)); + timestamps.push(curr); + } + let res = score(&BeaconingInput { timestamps }).unwrap(); + assert!(res.score >= 0.40, "seed {seed} score {} < 0.40", res.score); + if res.score < min_score { + min_score = res.score; + } + if res.score > max_score { + max_score = res.score; + } + let rel_err = (res.period_detected_seconds - 14400.0).abs() / 14400.0; + if rel_err < 0.10 { + period_within_10pct += 1; + } + } + println!( + "Test 3 (cumulative jitter) 50 seeds: min={min_score:.4}, max={max_score:.4}, period_hits={period_within_10pct}/50" + ); + assert!( + period_within_10pct >= 45, + "period hits: {period_within_10pct}/50" + ); + } + + #[test] + fn test_4_poisson_random() { + let mut min_score = f64::MAX; + let mut max_score = f64::MIN; + let mean = 7.0 * 86400.0 / 40.0; + + for seed in 1..=50 { + let mut rng = SplitMix64::new(seed); + let mut timestamps = Vec::with_capacity(40); + let mut curr = 0.0; + for _ in 0..40 { + curr += rng.exponential(mean); + timestamps.push(curr); + } + let res = score(&BeaconingInput { timestamps }).unwrap(); + assert!(res.score < 0.30, "seed {seed} score {} >= 0.30", res.score); + if res.score < min_score { + min_score = res.score; + } + if res.score > max_score { + max_score = res.score; + } + } + println!("Test 4 (Poisson) 50 seeds: min={min_score:.4}, max={max_score:.4}"); + } + + #[test] + fn test_5_sentinel_q3_eq_q1() { + let mut timestamps = Vec::with_capacity(25); + for k in 0..25 { + timestamps.push(k as f64 * 1000.0); + } + let res = score(&BeaconingInput { timestamps }).unwrap(); + assert_eq!(res.skew_bowley, 0.0); + assert!(res.degenerate_quartiles); + assert!(!res.score.is_nan() && !res.score.is_infinite()); + } + + #[test] + fn test_6_median_delta_zero() { + let timestamps = vec![1000.0; 25]; + let res = score(&BeaconingInput { timestamps }).unwrap(); + assert_eq!(res.status, "degenerate_deltas"); + assert!(!res.score.is_nan() && !res.score.is_infinite()); + } + + #[test] + fn test_7_insufficient_data() { + let timestamps: Vec = (0..19).map(|k| k as f64 * 1000.0).collect(); + let res = score(&BeaconingInput { timestamps }).unwrap(); + assert_eq!(res.status, "insufficient_data"); + assert_eq!(res.score, 0.0); + } + + #[test] + fn test_8_determinism() { + let timestamps: Vec = (0..42).map(|k| k as f64 * 14400.0).collect(); + let s1 = score(&BeaconingInput { + timestamps: timestamps.clone(), + }) + .unwrap(); + let s2 = score(&BeaconingInput { timestamps }).unwrap(); + let j1 = serde_json::to_vec(&s1).unwrap(); + let j2 = serde_json::to_vec(&s2).unwrap(); + assert_eq!(j1, j2); + } +} diff --git a/crates/acaua-core/src/storage.rs b/crates/acaua-core/src/storage.rs index a75a61b..4cdc81d 100644 --- a/crates/acaua-core/src/storage.rs +++ b/crates/acaua-core/src/storage.rs @@ -1,5 +1,9 @@ -use crate::error::Result; +use crate::error::{AcauaError, Result}; +use chrono::NaiveDate; use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::collections::HashSet; +use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug, Clone, Serialize)] @@ -18,32 +22,669 @@ impl DatasetSummary { partitions: 0, ttl_days: 7, backend: "duckdb-parquet", - status: "skeleton", + status: "empty", } } } +#[derive(Debug, Clone, Default)] +pub struct IngestionStats { + pub files_seen: usize, + pub records_ingested: u64, + pub lines_skipped: u64, + pub partitions_affected: usize, +} + +#[derive(Debug, Clone)] +pub struct TupleTimestamps { + pub orig_h: String, + pub resp_h: String, + pub resp_p: u16, + pub timestamps: Vec, +} + #[derive(Debug, Clone)] pub struct Storage { root: PathBuf, } +#[derive(Debug, Clone)] +pub struct ConnRecord { + pub ts: f64, + pub uid: String, + pub orig_h: String, + pub orig_p: u16, + pub resp_h: String, + pub resp_p: u16, + pub proto: String, + pub service: Option, + pub duration: Option, +} + impl Storage { - pub fn open(root: impl AsRef) -> Self { - Self { - root: root.as_ref().to_path_buf(), - } + pub fn open(root: impl AsRef) -> Result { + let root = root.as_ref(); + fs::create_dir_all(root.join("raw").join("conn"))?; + fs::create_dir_all(root.join("tmp"))?; + + let canonical_root = fs::canonicalize(root)?; + + let test_file = canonical_root.join("tmp").join(".write_check"); + fs::write(&test_file, b"ok")?; + let _ = fs::remove_file(&test_file); + + Ok(Self { + root: canonical_root, + }) } pub fn root(&self) -> &Path { &self.root } + pub fn open_connection(&self) -> Result { + let conn = duckdb::Connection::open_in_memory()?; + let data_dir_str = self.root.to_string_lossy().replace('\'', "''"); + let tmp_dir_str = self.root.join("tmp").to_string_lossy().replace('\'', "''"); + + conn.execute_batch(&format!( + "SET TimeZone = 'UTC'; + SET temp_directory = '{tmp_dir_str}'; + SET autoinstall_known_extensions = false; + SET autoload_known_extensions = false; + SET allowed_directories = ['{data_dir_str}/']; + SET enable_external_access = false; + SET lock_configuration = true;" + ))?; + + Ok(conn) + } + + fn has_parquet_files(&self) -> bool { + let conn_dir = self.root.join("raw").join("conn"); + if let Ok(entries) = fs::read_dir(conn_dir) { + for entry in entries.flatten() { + if let Ok(sub) = fs::read_dir(entry.path()) { + for f in sub.flatten() { + if f.path().extension().and_then(|s| s.to_str()) == Some("parquet") { + return true; + } + } + } + } + } + false + } + pub fn summary(&self) -> Result { - Ok(DatasetSummary::empty()) + if !self.has_parquet_files() { + return Ok(DatasetSummary::empty()); + } + + let conn = self.open_connection()?; + let parquet_glob = self + .root + .join("raw") + .join("conn") + .join("*") + .join("*.parquet") + .to_string_lossy() + .replace('\'', "''"); + + let query = format!( + "SELECT count(*)::BIGINT, count(DISTINCT date)::BIGINT + FROM read_parquet('{parquet_glob}', hive_partitioning = true);" + ); + + let row = conn.query_row(&query, [], |r| { + let records: i64 = r.get(0)?; + let partitions: i64 = r.get(1)?; + Ok((records as u64, partitions as usize)) + }); + + match row { + Ok((records, partitions)) => Ok(DatasetSummary { + records, + partitions, + ttl_days: 7, + backend: "duckdb-parquet", + status: "ok", + }), + Err(_) => Ok(DatasetSummary::empty()), + } } pub fn prune_expired(&self) -> Result { - Ok(0) + let conn_dir = self.root.join("raw").join("conn"); + let entries = match fs::read_dir(&conn_dir) { + Ok(e) => e, + Err(_) => return Ok(0), + }; + + let mut dates = Vec::new(); + let mut dirs = Vec::new(); + + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + if let Some(name) = path.file_name().and_then(|n| n.to_str()) { + if let Some(date_str) = name.strip_prefix("date=") { + if let Ok(d) = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { + dates.push(d); + dirs.push((d, path)); + } + } + } + } + + if dates.is_empty() { + return Ok(0); + } + + let max_date = dates.into_iter().max().unwrap(); + let cutoff = max_date - chrono::Duration::days(7); + + let mut pruned = 0; + for (date, path) in dirs { + if date <= cutoff && fs::remove_dir_all(&path).is_ok() { + pruned += 1; + } + } + + Ok(pruned) + } + + pub fn parse_conn_log(content: &[u8]) -> (Vec, u64, HashSet) { + let mut records = Vec::new(); + let mut skipped = 0; + let mut dates = HashSet::new(); + + let mut separator = '\t'; + let mut unset_field = String::from("-"); + let mut fields: Vec = Vec::new(); + + let mut idx_ts = None; + let mut idx_uid = None; + let mut idx_orig_h = None; + let mut idx_orig_p = None; + let mut idx_resp_h = None; + let mut idx_resp_p = None; + let mut idx_proto = None; + let mut idx_service = None; + let mut idx_duration = None; + + let text = String::from_utf8_lossy(content); + + for line in text.lines() { + let line = line.trim_end_matches(['\r', '\n']); + if line.is_empty() { + continue; + } + + if line.starts_with('#') { + if let Some(rest) = line.strip_prefix("#separator ") { + let sep_str = rest.trim(); + if let Some(hex_code) = sep_str.strip_prefix("\\x") { + if let Ok(b) = u8::from_str_radix(hex_code, 16) { + separator = b as char; + } + } else if !sep_str.is_empty() { + separator = sep_str.chars().next().unwrap(); + } + } else if let Some(rest) = line.strip_prefix("#unset_field") { + unset_field = rest.trim().to_string(); + } else if let Some(rest) = line.strip_prefix("#fields") { + let rest = rest.trim(); + fields = rest + .split(separator) + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + idx_ts = fields.iter().position(|s| s == "ts"); + idx_uid = fields.iter().position(|s| s == "uid"); + idx_orig_h = fields.iter().position(|s| s == "id.orig_h"); + idx_orig_p = fields.iter().position(|s| s == "id.orig_p"); + idx_resp_h = fields.iter().position(|s| s == "id.resp_h"); + idx_resp_p = fields.iter().position(|s| s == "id.resp_p"); + idx_proto = fields.iter().position(|s| s == "proto"); + idx_service = fields.iter().position(|s| s == "service"); + idx_duration = fields.iter().position(|s| s == "duration"); + } + continue; + } + + // Must have fields mapped + let (i_ts, i_uid, i_oh, i_op, i_rh, i_rp, i_pr) = match ( + idx_ts, idx_uid, idx_orig_h, idx_orig_p, idx_resp_h, idx_resp_p, idx_proto, + ) { + (Some(a), Some(b), Some(c), Some(d), Some(e), Some(f), Some(g)) => { + (a, b, c, d, e, f, g) + } + _ => { + skipped += 1; + continue; + } + }; + + let parts: Vec<&str> = line.split(separator).collect(); + if parts.len() < fields.len() { + skipped += 1; + continue; + } + + let ts: f64 = match parts[i_ts].parse() { + Ok(v) => v, + Err(_) => { + skipped += 1; + continue; + } + }; + + let orig_p: u16 = match parts[i_op].parse() { + Ok(v) => v, + Err(_) => { + skipped += 1; + continue; + } + }; + + let resp_p: u16 = match parts[i_rp].parse() { + Ok(v) => v, + Err(_) => { + skipped += 1; + continue; + } + }; + + let service = idx_service.and_then(|idx| { + let val = parts.get(idx)?; + if *val == unset_field.as_str() || *val == "-" { + None + } else { + Some(val.to_string()) + } + }); + + let duration = idx_duration.and_then(|idx| { + let val = parts.get(idx)?; + if *val == unset_field.as_str() || *val == "-" { + None + } else { + val.parse::().ok() + } + }); + + // Track day partition (UTC) + let date_ts = chrono::DateTime::from_timestamp(ts.floor() as i64, 0); + if let Some(dt) = date_ts { + dates.insert(dt.format("%Y-%m-%d").to_string()); + } + + records.push(ConnRecord { + ts, + uid: parts[i_uid].to_string(), + orig_h: parts[i_oh].to_string(), + orig_p, + resp_h: parts[i_rh].to_string(), + resp_p, + proto: parts[i_pr].to_string(), + service, + duration, + }); + } + + (records, skipped, dates) + } + + pub fn ingest_file(&self, path: impl AsRef) -> Result<(u64, u64, usize)> { + let path = path.as_ref(); + let content = fs::read(path)?; + let mut hasher = Sha256::new(); + hasher.update(&content); + let hash = hasher.finalize(); + let mut sha16 = String::with_capacity(16); + for b in &hash[..8] { + use std::fmt::Write; + let _ = write!(sha16, "{b:02x}"); + } + + let (records, skipped, dates) = Self::parse_conn_log(&content); + if records.is_empty() { + return Ok((0, skipped, 0)); + } + + let conn = self.open_connection()?; + conn.execute_batch( + "CREATE TEMP TABLE t ( + ts DOUBLE, + uid VARCHAR, + orig_h VARCHAR, + orig_p USMALLINT, + resp_h VARCHAR, + resp_p USMALLINT, + proto VARCHAR, + service VARCHAR, + duration DOUBLE + );", + )?; + + { + let mut appender = conn.appender("t")?; + for r in &records { + appender.append_row(duckdb::params![ + r.ts, r.uid, r.orig_h, r.orig_p, r.resp_h, r.resp_p, r.proto, r.service, + r.duration + ])?; + } + } + + let raw_conn_dir = self + .root + .join("raw") + .join("conn") + .to_string_lossy() + .replace('\'', "''"); + + let copy_sql = format!( + "COPY ( + SELECT *, CAST(to_timestamp(ts) AS DATE) AS date FROM t + ) TO '{raw_conn_dir}' ( + FORMAT parquet, + PARTITION_BY (date), + FILENAME_PATTERN 'src_{sha16}_{{i}}', + OVERWRITE_OR_IGNORE + );" + ); + + conn.execute_batch(©_sql)?; + conn.execute_batch("DROP TABLE t;")?; + + Ok((records.len() as u64, skipped, dates.len())) + } + + pub fn ingest_dir(&self, directory: impl AsRef) -> Result { + let directory = directory.as_ref(); + if !directory.is_dir() { + return Err(AcauaError::InvalidInput(format!( + "{} is not a directory", + directory.display() + ))); + } + + let canonical_dir = fs::canonicalize(directory)?; + + let mut stats = IngestionStats::default(); + let mut all_dates = HashSet::new(); + + let entries = fs::read_dir(&canonical_dir)?; + for entry in entries.flatten() { + let path = entry.path(); + // Path confinement: do not follow symlink to outside directory + if let Ok(canon) = fs::canonicalize(&path) { + if !canon.starts_with(&canonical_dir) { + continue; + } + } else { + continue; + } + + let file_name = match path.file_name().and_then(|n| n.to_str()) { + Some(n) => n, + None => continue, + }; + + // Skip .gz archives with note + if file_name.ends_with(".gz") { + continue; + } + + // Only conn.log and conn.*.log directly in directory + let is_conn = file_name == "conn.log" + || (file_name.starts_with("conn.") && file_name.ends_with(".log")); + + if is_conn && path.is_file() { + stats.files_seen += 1; + match self.ingest_file(&path) { + Ok((recs, sk, affected)) => { + stats.records_ingested += recs; + stats.lines_skipped += sk; + for _ in 0..affected { + all_dates.insert(file_name.to_string()); + } + } + Err(_) => { + stats.lines_skipped += 1; + } + } + } + } + + stats.partitions_affected = all_dates.len(); + Ok(stats) + } + + pub fn query_7day_tuples(&self) -> Result> { + if !self.has_parquet_files() { + return Ok(Vec::new()); + } + + let conn = self.open_connection()?; + let parquet_glob = self + .root + .join("raw") + .join("conn") + .join("*") + .join("*.parquet") + .to_string_lossy() + .replace('\'', "''"); + + // Find max date present in data + let max_date_query = format!( + "SELECT max(date)::VARCHAR + FROM read_parquet('{parquet_glob}', hive_partitioning = true);" + ); + + let max_date: Option = conn + .query_row(&max_date_query, [], |r| r.get(0)) + .unwrap_or(None); + + let max_date_str = match max_date { + Some(d) if !d.is_empty() => d, + _ => return Ok(Vec::new()), + }; + + let query = format!( + "WITH daily AS ( + SELECT date, orig_h, resp_h, resp_p, list(ts ORDER BY ts) AS tss + FROM read_parquet('{parquet_glob}', hive_partitioning = true) + WHERE date > CAST('{max_date_str}' AS DATE) - INTERVAL 7 DAY + GROUP BY ALL + ) + SELECT orig_h, resp_h, resp_p, list_sort(flatten(list(tss))) AS timestamps + FROM daily + GROUP BY ALL + ORDER BY orig_h, resp_h, resp_p;" + ); + + let mut stmt = conn.prepare(&query)?; + let rows = stmt.query_map([], |row| { + let orig_h: String = row.get(0)?; + let resp_h: String = row.get(1)?; + let resp_p: u16 = row.get(2)?; + let timestamps_val: duckdb::types::Value = row.get(3)?; + let mut timestamps = Vec::new(); + if let duckdb::types::Value::List(items) = timestamps_val { + for item in items { + if let duckdb::types::Value::Double(t) = item { + timestamps.push(t); + } + } + } + Ok(TupleTimestamps { + orig_h, + resp_h, + resp_p, + timestamps, + }) + })?; + + let mut result = Vec::new(); + for r in rows { + result.push(r?); + } + Ok(result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const CONN_LOG_SAMPLE_1: &str = "#separator \\x09\n\ +#set_separator\t,\n\ +#empty_field\t(empty)\n\ +#unset_field\t-\n\ +#path\tconn\n\ +#open\t2026-10-31-12-00-00\n\ +#fields\tts\tuid\tid.orig_h\tid.orig_p\tid.resp_h\tid.resp_p\tproto\tservice\tduration\tip_proto\n\ +#types\ttime\tstring\taddr\tport\taddr\tport\tenum\tstring\tinterval\tcount\n\ +1793461200.000000\tCuid1\t192.168.1.10\t45000\t10.0.0.1\t443\ttcp\tssl\t1.234\t6\n\ +1793461205.000000\tCuid2\t192.168.1.10\t45001\t10.0.0.1\t443\ttcp\t-\t-\t6\n\ +#close\t2026-10-31-12-01-00\n"; + + const CONN_LOG_PERMUTED_COLS: &str = "#separator \\x09\n\ +#fields\tid.resp_p\tid.orig_h\tservice\tuid\tduration\tproto\tts\tid.resp_h\tid.orig_p\n\ +443\t192.168.1.10\thttp\tCuid3\t0.5\ttcp\t1793461210.000000\t10.0.0.1\t45002\n"; + + #[test] + fn test_parser_extracts_correct_fields_and_unset_as_none() { + let (records, skipped, dates) = Storage::parse_conn_log(CONN_LOG_SAMPLE_1.as_bytes()); + assert_eq!(skipped, 0); + assert_eq!(records.len(), 2); + assert_eq!(dates.len(), 1); + + assert_eq!(records[0].uid, "Cuid1"); + assert_eq!(records[0].orig_h, "192.168.1.10"); + assert_eq!(records[0].orig_p, 45000); + assert_eq!(records[0].resp_h, "10.0.0.1"); + assert_eq!(records[0].resp_p, 443); + assert_eq!(records[0].proto, "tcp"); + assert_eq!(records[0].service.as_deref(), Some("ssl")); + assert_eq!(records[0].duration, Some(1.234)); + + assert_eq!(records[1].uid, "Cuid2"); + assert_eq!(records[1].service, None); + assert_eq!(records[1].duration, None); + } + + #[test] + fn test_parser_maps_by_name_not_position() { + let (records, skipped, _) = Storage::parse_conn_log(CONN_LOG_PERMUTED_COLS.as_bytes()); + assert_eq!(skipped, 0); + assert_eq!(records.len(), 1); + let r = &records[0]; + assert_eq!(r.uid, "Cuid3"); + assert_eq!(r.orig_h, "192.168.1.10"); + assert_eq!(r.orig_p, 45002); + assert_eq!(r.resp_h, "10.0.0.1"); + assert_eq!(r.resp_p, 443); + assert_eq!(r.proto, "tcp"); + assert_eq!(r.service.as_deref(), Some("http")); + assert_eq!(r.duration, Some(0.5)); + } + + #[test] + fn test_storage_roundtrip_and_idempotence() { + let temp_dir = tempfile::tempdir().unwrap(); + let store = Storage::open(temp_dir.path()).unwrap(); + + let log_file = temp_dir.path().join("conn.log"); + fs::write(&log_file, CONN_LOG_SAMPLE_1).unwrap(); + + let (ingested1, skipped1, _) = store.ingest_file(&log_file).unwrap(); + assert_eq!(ingested1, 2); + assert_eq!(skipped1, 0); + + let summary1 = store.summary().unwrap(); + assert_eq!(summary1.records, 2); + assert_eq!(summary1.partitions, 1); + + // Idempotence: re-ingesting the exact same file does not duplicate records + let (ingested2, skipped2, _) = store.ingest_file(&log_file).unwrap(); + assert_eq!(ingested2, 2); + assert_eq!(skipped2, 0); + + let summary2 = store.summary().unwrap(); + assert_eq!(summary2.records, 2); + + // Ingesting a different file for the same tuple/day adds to it + let log_file_2 = temp_dir.path().join("conn.extra.log"); + fs::write(&log_file_2, CONN_LOG_PERMUTED_COLS).unwrap(); + store.ingest_file(&log_file_2).unwrap(); + + let summary3 = store.summary().unwrap(); + assert_eq!(summary3.records, 3); + + let tuples = store.query_7day_tuples().unwrap(); + assert_eq!(tuples.len(), 1); + assert_eq!(tuples[0].orig_h, "192.168.1.10"); + assert_eq!(tuples[0].resp_h, "10.0.0.1"); + assert_eq!(tuples[0].resp_p, 443); + assert_eq!(tuples[0].timestamps.len(), 3); + assert_eq!(tuples[0].timestamps[0], 1793461200.0); + assert_eq!(tuples[0].timestamps[1], 1793461205.0); + assert_eq!(tuples[0].timestamps[2], 1793461210.0); + } + + #[test] + fn test_storage_utc_timezone_partitioning() { + let temp_dir = tempfile::tempdir().unwrap(); + let store = Storage::open(temp_dir.path()).unwrap(); + + // 2026-10-31 02:00:00 UTC = 1793412000 + let log = "#separator \\x09\n#fields\tts\tuid\tid.orig_h\tid.orig_p\tid.resp_h\tid.resp_p\tproto\n1793412000.000000\tu1\t10.0.0.1\t1000\t10.0.0.2\t80\ttcp\n"; + let log_file = temp_dir.path().join("conn.log"); + fs::write(&log_file, log).unwrap(); + store.ingest_file(&log_file).unwrap(); + + let partition_dir = temp_dir + .path() + .join("raw") + .join("conn") + .join("date=2026-10-31"); + assert!( + partition_dir.is_dir(), + "partition date=2026-10-31 should exist regardless of local timezone" + ); + } + + #[test] + fn test_storage_sandbox_blocks_unauthorized_paths() { + let temp_dir = tempfile::tempdir().unwrap(); + let store = Storage::open(temp_dir.path()).unwrap(); + let conn = store.open_connection().unwrap(); + + let res = conn.execute_batch("SELECT * FROM read_csv('/etc/passwd');"); + assert!(res.is_err(), "sandbox must block access outside data_dir"); + } + + #[test] + fn test_storage_prune_expired() { + let temp_dir = tempfile::tempdir().unwrap(); + let store = Storage::open(temp_dir.path()).unwrap(); + + let raw_conn = temp_dir.path().join("raw").join("conn"); + // Day 1 (10 days ago) and Day 10 (recent) + let old_part = raw_conn.join("date=2026-10-01"); + let new_part = raw_conn.join("date=2026-10-11"); + fs::create_dir_all(&old_part).unwrap(); + fs::create_dir_all(&new_part).unwrap(); + + let pruned = store.prune_expired().unwrap(); + assert_eq!(pruned, 1); + assert!(!old_part.exists()); + assert!(new_part.exists()); } } diff --git a/crates/acaua-core/src/update.rs b/crates/acaua-core/src/update.rs index 04d38a4..3bfddb6 100644 --- a/crates/acaua-core/src/update.rs +++ b/crates/acaua-core/src/update.rs @@ -1,25 +1,13 @@ 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, -} - -#[derive(Debug, Deserialize)] -struct Asset { - name: String, - browser_download_url: String, } #[derive(Debug, Clone, Serialize)] @@ -46,42 +34,6 @@ fn curl_text(url: &str) -> Result { 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 { let body = match curl_text(LATEST_RELEASE_URL) { Ok(body) => body, @@ -103,492 +55,14 @@ pub fn check(current_version: &str) -> Result { }) } -pub fn apply(current_exe: &Path) -> Result { - 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 { - 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 { - #[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 { - 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 { - 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(current_exe: &Path, on_progress: F) -> Result -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()); + fn parses_latest_release_tag() { + let json = r#"{"tag_name": "v0.2.0"}"#; + let release: Release = serde_json::from_str(json).unwrap(); + assert_eq!(release.tag_name, "v0.2.0"); } } diff --git a/crates/acaua-core/src/zeek.rs b/crates/acaua-core/src/zeek.rs index bbcb0cd..9633310 100644 --- a/crates/acaua-core/src/zeek.rs +++ b/crates/acaua-core/src/zeek.rs @@ -1,7 +1,11 @@ use crate::error::{AcauaError, Result}; use crate::fusion::Alert; +use crate::pillar1::{self, BeaconingInput}; +use crate::storage::Storage; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::path::Path; +use time::OffsetDateTime; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ZeekLogKind { @@ -26,7 +30,10 @@ pub struct AnalysisReport { pub status: &'static str, } -pub fn analyze_dir(directory: impl AsRef) -> Result { +pub fn analyze_dir( + directory: impl AsRef, + data_dir: impl AsRef, +) -> Result { let directory = directory.as_ref(); if !directory.is_dir() { return Err(AcauaError::InvalidInput(format!( @@ -34,12 +41,78 @@ pub fn analyze_dir(directory: impl AsRef) -> Result { directory.display() ))); } + + let store = Storage::open(data_dir)?; + let stats = store.ingest_dir(directory)?; + let tuples = store.query_7day_tuples()?; + + let mut alerts = Vec::new(); + + for tuple in tuples { + if tuple.timestamps.len() < 20 { + continue; + } + + let beacon_score = pillar1::score(&BeaconingInput { + timestamps: tuple.timestamps.clone(), + })?; + + if beacon_score.score >= 0.40 { + let latest_ts = tuple.timestamps.last().copied().unwrap_or(0.0); + let latest_date_str = match OffsetDateTime::from_unix_timestamp(latest_ts.floor() as i64) + { + Ok(dt) => { + let format = time::macros::format_description!("[year]-[month]-[day]"); + dt.date() + .format(&format) + .unwrap_or_else(|_| "unknown-date".to_string()) + } + Err(_) => "unknown-date".to_string(), + }; + + let mut hasher = Sha256::new(); + let raw_id = format!( + "1|{}|{}|{}|{}", + tuple.orig_h, tuple.resp_h, tuple.resp_p, latest_date_str + ); + hasher.update(raw_id.as_bytes()); + let hash = hasher.finalize(); + let mut alert_id = String::with_capacity(16); + for b in &hash[..8] { + use std::fmt::Write; + let _ = write!(alert_id, "{b:02x}"); + } + + let summary = format!( + "Beaconing detected: {}:{} -> {}:{} (period ~{:.0}s, score {:.3})", + tuple.orig_h, + "any", + tuple.resp_h, + tuple.resp_p, + beacon_score.period_detected_seconds, + beacon_score.score + ); + + let evidence = serde_json::to_value(&beacon_score)?; + let created_at = OffsetDateTime::now_utc().to_string(); + + alerts.push(Alert { + id: alert_id, + pillar: 1, + score: beacon_score.score, + summary, + evidence, + created_at, + }); + } + } + Ok(AnalysisReport { directory: directory.display().to_string(), - logs_seen: 0, - records: 0, - alerts: Vec::new(), - status: "skeleton", + logs_seen: stats.files_seen, + records: stats.records_ingested, + alerts, + status: "ok", }) } diff --git a/crates/acaua-core/tests/fixtures/beacon/conn.log b/crates/acaua-core/tests/fixtures/beacon/conn.log new file mode 100644 index 0000000..bcd0d9f --- /dev/null +++ b/crates/acaua-core/tests/fixtures/beacon/conn.log @@ -0,0 +1,76 @@ +#separator \x09 +#set_separator , +#empty_field (empty) +#unset_field - +#path conn +#open 2026-10-31-00-00-00 +#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration ip_proto +#types time string addr port addr port enum string interval count +1793400000.000000 Cbeacon000 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793414400.000000 Cbeacon001 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793420401.205745 Crandom000 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793420907.782526 Crandom001 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793427340.263808 Crandom002 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793428800.000000 Cbeacon002 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793432391.987521 Crandom003 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793443200.000000 Cbeacon003 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793457600.000000 Cbeacon004 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793459063.840977 Crandom004 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793472000.000000 Cbeacon005 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793481647.301153 Crandom005 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793486400.000000 Cbeacon006 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793500800.000000 Cbeacon007 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793515200.000000 Cbeacon008 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793526193.063144 Crandom006 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793528012.111237 Crandom007 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793529600.000000 Cbeacon009 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793538973.034425 Crandom008 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793539578.037969 Crandom009 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793544000.000000 Cbeacon010 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793544512.371888 Crandom010 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793558400.000000 Cbeacon011 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793558590.682445 Crandom011 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793559128.570519 Crandom012 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793563562.403902 Crandom013 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793572800.000000 Cbeacon012 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793584552.243927 Crandom014 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793587200.000000 Cbeacon013 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793600298.829012 Crandom015 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793601600.000000 Cbeacon014 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793605279.357391 Crandom016 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793616000.000000 Cbeacon015 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793623075.531517 Crandom017 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793630400.000000 Cbeacon016 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793644800.000000 Cbeacon017 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793656230.293402 Crandom018 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793656360.692773 Crandom019 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793659200.000000 Cbeacon018 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793673600.000000 Cbeacon019 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793688000.000000 Cbeacon020 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793689140.010015 Crandom020 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793702400.000000 Cbeacon021 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793713095.808834 Crandom021 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793716800.000000 Cbeacon022 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793721413.710564 Crandom022 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793724793.435926 Crandom023 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793731200.000000 Cbeacon023 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793745600.000000 Cbeacon024 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793760000.000000 Cbeacon025 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793774400.000000 Cbeacon026 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793787823.888895 Crandom024 192.168.1.50 49153 198.51.100.2 80 tcp http 0.050 6 +1793788800.000000 Cbeacon027 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793803200.000000 Cbeacon028 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793817600.000000 Cbeacon029 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793832000.000000 Cbeacon030 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793846400.000000 Cbeacon031 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793860800.000000 Cbeacon032 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793875200.000000 Cbeacon033 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793889600.000000 Cbeacon034 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793904000.000000 Cbeacon035 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793918400.000000 Cbeacon036 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793932800.000000 Cbeacon037 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793947200.000000 Cbeacon038 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793961600.000000 Cbeacon039 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793976000.000000 Cbeacon040 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +1793990400.000000 Cbeacon041 192.168.1.50 49152 198.51.100.1 443 tcp ssl 0.123 6 +#close 2026-10-31-23-59-59 diff --git a/crates/acaua-core/tests/smoke.rs b/crates/acaua-core/tests/smoke.rs index fec4978..b4a2309 100644 --- a/crates/acaua-core/tests/smoke.rs +++ b/crates/acaua-core/tests/smoke.rs @@ -7,7 +7,7 @@ fn pillar1_returns_a_skeleton_score() { }; let score = pillar1::score(&input).unwrap(); assert_eq!(score.observations, 3); - assert_eq!(score.status, "skeleton"); + assert_eq!(score.status, "insufficient_data"); } #[test] @@ -44,7 +44,7 @@ fn fusion_returns_no_alerts_yet() { #[test] fn storage_reports_the_reserved_backend() { - let store = storage::Storage::open(std::env::temp_dir()); + let store = storage::Storage::open(std::env::temp_dir()).unwrap(); let summary = store.summary().unwrap(); assert_eq!(summary.ttl_days, 7); assert_eq!(summary.records, 0); @@ -52,7 +52,8 @@ fn storage_reports_the_reserved_backend() { #[test] fn zeek_rejects_a_missing_directory() { - let error = zeek::analyze_dir("/definitely/not/a/zeek/directory").unwrap_err(); + let error = + zeek::analyze_dir("/definitely/not/a/zeek/directory", std::env::temp_dir()).unwrap_err(); assert!(matches!(error, AcauaError::InvalidInput(_))); } diff --git a/docker-compose.yml b/docker-compose.yml index 67e05d4..4f7333b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,45 @@ +# Acauã stack: Zeek captures traffic and writes hourly-rotated logs; Acauã +# reads them read-only and serves the API and the web dashboard. +# +# cp .env.example .env # set ZEEK_INTERFACE (authorized network only) +# docker compose pull && docker compose up -d # operators: published image +# docker compose up -d --build # developers: build locally +name: acaua + services: zeek: - image: zeek/zeek:latest + image: ${ZEEK_IMAGE:-zeek/zeek:9.0.1} 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"] + cap_drop: [ALL] + cap_add: [NET_RAW, NET_ADMIN] + working_dir: /logs + command: + - zeek + - -i + - ${ZEEK_INTERFACE:-eth0} + - Log::default_logdir=/logs + - Log::default_rotation_interval=1hr + - Log::default_rotation_dir=/logs/archive volumes: - - ./data/zeek:/logs + - zeek-logs:/logs restart: unless-stopped acaua: + image: ${ACAUA_IMAGE:-git.lucasrguerra.dev.br/ciberfauna/acaua}:${ACAUA_VERSION:-latest} build: . - command: ["serve", "--bind", "0.0.0.0:8787", "--data-dir", "/data/acaua"] ports: - - "127.0.0.1:8787:8787" + - "${ACAUA_PUBLISH:-127.0.0.1:8787}:8787" volumes: - - ./data/zeek:/data/zeek:ro - - ./data/acaua:/data/acaua + - zeek-logs:/data/zeek:ro + - acaua-data:/data/acaua + read_only: true + tmpfs: + - /tmp + cap_drop: [ALL] + security_opt: + - no-new-privileges:true restart: unless-stopped + +volumes: + zeek-logs: + acaua-data: diff --git a/docs/architecture.md b/docs/architecture.md index af8eb72..b8b7b32 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2,8 +2,8 @@ **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 +Acauã is a single Rust product: one daemon/CLI, one shared core, and one +embedded web dashboard. 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. @@ -11,19 +11,14 @@ corrections applied to the original blueprint. | Layer | Choice | Why | |---|---|---| -| Core, CLI and daemon | Rust 2021 | One toolchain, memory safety, no runtime dependency | +| Core, CLI and daemon | Rust 2021 (MSRV 1.88) | 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 | +| Web Dashboard | Vanilla HTML/CSS/JS | No frontend build step, assets embedded in the binary via `rust-embed` | +| Analytical store | DuckDB + Parquet | Mandatory embedded columnar store, linked to official prebuilt `libduckdb.so` | +| Deployment | Docker / docker-compose | Collective SOC daemon, unprivileged non-root user, read-only rootfs | | 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. +The workspace has two crates: `acaua-core` and `acaua-cli`. Both use `version.workspace = true`. ## Components @@ -35,20 +30,20 @@ Zeek logs ──► acaua-core::zeek ──► normalized events ──► stora ├──► pillar3 (protocol anomalies) └──► pillar4 (fusion) ──► alerts + evidence │ -acaua-cli ──► axum API ──► embedded UI ◄── Tauri shell ─────────────┘ +acaua-cli (serve) ──► axum API ──► embedded web dashboard ──────────┘ ``` - `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. +- `storage`: DuckDB in-memory engine over daily partitioned Parquet files with a + seven-day TTL. - `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. +- `doctor`: local environment checks; reports prebuilt DuckDB linkage and + containerized Zeek status. +- `update`: checks for updates against the Gitea Releases API. ## The six corrections @@ -58,117 +53,107 @@ 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 +P(T) = (2 / N) * [ (sum cos(2*pi*t_i / T))^2 + (sum sin(2*pi*t_i / T))^2 ] ``` -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. +Generalized Lomb–Scargle is reserved for the future extension that evaluates a +real response attribute (such as payload size or duration) against time. -### 2. Bowley skew with an explicit sentinel +### 2. Guard the zero-dispersion case in MADM -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. +A synthetic or unjittered beacon produces equal intervals. The median absolute +deviation from the median (MADM) is then `0.0`. Dividing by `0.0` or treating it +as a missing value is wrong: zero dispersion is the most suspicious outcome. The +implementation guards this case with an explicit score assignment (`1.0`) and a +documented sentinel. -### 3. Jensen–Shannon, not raw KL +### 3. Replace Pearson skewness with Bowley skewness -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. +Sample skewness based on the third central moment is sensitive to single +outliers and can diverge. Bowley skewness uses quartiles: -### 4. Persistent sessions get their own detector +```text +B = (Q3 + Q1 - 2*Q2) / (Q3 - Q1) +``` -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. +It is bounded in `[-1.0, 1.0]` by definition and resistant to extreme outliers. +When `Q3 == Q1` (concentrated data), the implementation assigns `0.0` with a +sentinel flag. -### 5. JA4 license caution +### 4. Isolate the Python research workspace -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. +`research/` has its own virtual environment and dependencies. It is excluded +from the Cargo workspace, ignored by Docker builds and CI releases, and tested +only when research validation scripts are invoked directly. -### 6. Determinism and per-alert JSON evidence +### 5. Documented degraded mode for NetFlow/IPFIX -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. +NetFlow v9 and IPFIX do not carry DNS query names, TLS SNI, certificate chains +or HTTP headers. Any analysis that depends on protocol features is unavailable. +Acauã documents this as a degraded mode and never claims parity between flow +records and full Zeek logs. -## Data flow +### 6. Single HTTP and RPC boundary -1. `acaua analyze ` 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. +The daemon exposes an Axum API on loopback by default. The embedded web UI +communicates with the daemon exclusively via standard HTTP calls. ## API -| Method | Path | Purpose | +The HTTP API is served by `acaua serve`: + +| Method | Path | Meaning | |---|---|---| | GET | `/api/health` | Liveness and version | -| GET | `/api/status` | Uptime, data directory, pillar state | +| GET | `/api/status` | Uptime, data directory, dataset summary | | GET | `/api/alerts` | Paginated alerts with evidence | -| POST | `/api/analyze` | Analyze a dataset path | -| POST | `/api/update` | Check for a CLI update | +| POST | `/api/analyze` | Ingest and analyze a dataset directory | +| GET | `/api/update` | Check for newer releases | | GET | `/logo.png` | Embedded brand asset | -| GET | `/*` | Embedded UI fallback | +| GET | `/*` | Embedded UI fallback (`rust-embed`) | -`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. +`serve` binds to `127.0.0.1:8787` by default. Exposing it on an external network +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. +DuckDB is a mandatory dependency linked dynamically to the official prebuilt +`libduckdb.so` (version 1.5.6 pinned with SHA-256 in `scripts/fetch-libduckdb.sh`). +Parquet, JSON and ICU extensions are statically built into the official library. +The schema consists of daily partitioned Parquet files under the configured +data directory, with in-memory DuckDB querying and a seven-day TTL. ## 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. +Updates for Acauã are delivered via container images (`docker compose pull && docker compose up -d`). +The `acaua update` command queries the Gitea Releases API for `ciberfauna/acaua` and +notifies operators when a newer version tag is available. Binary self-overwrite +(`update --apply`) is removed: in a collective SOC daemon running under container +orchestration, updates must be controlled by image pulls rather than self-mutating +processes. ## 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/`. +1. **Python service with FastAPI/ClickHouse:** Rejected. A Python runtime introduces + heavy container footprints and unnecessary memory boundaries; ClickHouse adds an + external server operational burden for datasets that fit an embedded columnar store. + The product is 100% Rust. +2. **Desktop application (Tauri / AppImage / NSIS):** Rejected. Unlike Guará (local + raw disk forensics) and Sabiá (Wi-Fi monitor-mode hardware), Acauã has no local + hardware dependencies: it is a multi-user SOC daemon consuming Zeek logs. A desktop + wrapper adds maintenance and security surface without operational benefits. ## 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). +- **Docker Compose (Official):** Multi-arch container image (`git.lucasrguerra.dev.br/ciberfauna/acaua`), + running as unprivileged user `acaua` (UID 10001), read-only root filesystem, dropped capabilities. +- **Native compilation:** `scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb` followed by `cargo build --release -p acaua-cli`. ## 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. +over existing data. ## Versioning and compatibility diff --git a/docs/cli.md b/docs/cli.md index cac819e..60ef663 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -2,23 +2,22 @@ **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. +The `acaua` binary serves both as the CLI command runner and the background daemon. +This page describes the commands available in the engine. ```text acaua [options] Commands: - serve Start the API and embedded UI + serve Start the API and embedded web dashboard analyze Analyze a Zeek log directory doctor Check the local environment - update Check or apply a CLI update + update Check for available releases ``` ## `acaua serve` -Starts the HTTP API and the embedded UI. +Starts the HTTP API and the embedded web dashboard. ```bash acaua serve --bind 127.0.0.1:8787 --data-dir .acaua @@ -40,14 +39,8 @@ 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. +Validates the target directory, ingests `conn.log` traffic into Parquet partitions, +and runs threat detection algorithms. ## `acaua doctor` @@ -56,52 +49,34 @@ 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. +Runs environment checks: +- `zeek`: verifies `zeek --version` or detects containerized execution (`ACAUA_RUNTIME=container`). +- `curl`: confirms HTTP download availability. +- `duckdb`: confirms dynamic linkage against `libduckdb.so` in runtime. +- `research`: verifies that the research workspace remains isolated. ## `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. +Queries the Gitea Releases API, compares the latest tag against the running +version, and prints upgrade instructions (`docker compose pull && docker compose up -d` +when running inside a container). ## Exit codes | Code | Meaning | |---|---| | `0` | Success | -| `1` | Runtime error (invalid path, failed update, I/O error) | +| `1` | Runtime error (invalid path, query error, 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. +| Variable | Meaning | +|---|---| +| `ACAUA_RUNTIME` | Set to `container` inside Docker images to adapt service checks | +| `DUCKDB_LIB_DIR` | Path to directory containing `libduckdb.so` when compiling natively | +| `LD_LIBRARY_PATH` | Shared library load path for `libduckdb.so` outside standard paths | diff --git a/docs/docker.md b/docs/docker.md index 7409c29..c01df7a 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -2,66 +2,78 @@ **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. +Acauã is distributed as an official container image: +`git.lucasrguerra.dev.br/ciberfauna/acaua`. -## Build +Available image tags: +- `latest`: Most recent stable release. +- `X.Y.Z` / `X.Y`: Pinned semantic versions. +- `edge`: Built automatically from the `main` branch. + +The container runs as an unprivileged system user (`acaua`, UID 10001), with a +read-only root filesystem (`read_only: true`), all capabilities dropped +(`cap_drop: [ALL]`), and security hardening (`no-new-privileges:true`). + +## Compose deployment + +The recommended production and lab deployment uses Docker Compose: ```bash -docker build -t acaua . -docker run --rm -p 127.0.0.1:8787:8787 -v "$PWD/data/acaua:/data/acaua" acaua +cp .env.example .env # configure ZEEK_INTERFACE (authorized networks only) +docker compose pull && docker compose up -d ``` -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 +To build and run locally from source: ```bash -cp .env.example .env # optional: set ZEEK_INTERFACE -docker compose up --build +docker compose up -d --build ``` -The Compose file starts two services: +The stack coordinates 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` | +| Service | Image | Purpose | Security profile | +|---|---|---|---| +| `zeek` | `zeek/zeek:9.0.1` | Network packet capture and hourly log rotation | Host network, `NET_RAW` and `NET_ADMIN` capabilities | +| `acaua` | `git.lucasrguerra.dev.br/ciberfauna/acaua` | Analytical processing, API and web dashboard | Unprivileged (UID 10001), read-only rootfs, no capabilities | -`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. +## Volumes and data layout -## Data layout +The stack uses named volumes to manage ownership and permissions cleanly: -```text -data/ - zeek/ # Zeek logs, written by the zeek service - acaua/ # store, exports and runtime files +- `zeek-logs`: Shared volume mounted at `/logs` in the Zeek service and mounted + **read-only** at `/data/zeek:ro` in the Acauã service. +- `acaua-data`: Dedicated read-write volume mounted at `/data/acaua` for DuckDB + analytical store, Parquet partitions and runtime files. + +To inspect or extract data on the host: + +```bash +docker compose cp acaua:/data/acaua ./local-acaua-backup ``` -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. +Or run a one-off inspection container: + +```bash +docker run --rm -v acaua_acaua-data:/data:ro debian:trixie-slim ls -la /data +``` + +## Updating + +Because Acauã runs as a multi-user collective SOC daemon, updates are performed +by pulling the latest verified image: + +```bash +docker compose pull && docker compose up -d +``` + +The running daemon shuts down cleanly on `SIGTERM` within sub-second intervals, +ensuring Parquet writes and DuckDB transactions are committed properly. ## 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. +- The `zeek` service requires packet capture privileges (`NET_RAW`, `NET_ADMIN`). + Run it only on interfaces and networks you are explicitly authorized to monitor. +- The default dashboard binding is loopback-only (`ACAUA_PUBLISH=127.0.0.1:8787`). + Do not expose the service on external interfaces without an authenticating reverse proxy. +- The image contains no telemetry. Outbound traffic is restricted to operator-initiated + update checks against the Gitea Releases API. diff --git a/docs/interface.md b/docs/interface.md index 5fc899b..992cb50 100644 --- a/docs/interface.md +++ b/docs/interface.md @@ -2,66 +2,39 @@ **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. +The interface is a vanilla HTML/CSS/JS dashboard embedded directly into the +`acaua` binary via `rust-embed` and served by `acaua serve`. There is no +frontend build step and no heavy framework runtime. ## 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. +- **Sidebar:** brand logo, navigation, daemon connection indicator, version and update check. +- **Overview:** monitoring scope notice, authorized-use warning and four detection pillar cards. +- **Analysis:** dataset path entry, analyze button, refresh and raw JSON output inspection. +- **Alerts:** list of alerts with deterministic evidence metadata. +- **Settings:** language and theme preferences, stored in browser local storage. ## 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. +The source text is Portuguese. `i18n.js` applies English translations from +`i18n-en.js` at runtime and persists the selection in `localStorage`. `Auto` +detects the browser language. The automated test `node scripts/check-ui-i18n.mjs` +guarantees that every visible Portuguese string has an exact English pair. ## Theme -`Auto`, light and dark are supported. `Auto` follows `prefers-color-scheme`. The -choice is stored locally; no preference leaves the machine. +Light, dark and `Auto` modes are supported. `Auto` reacts to system +`prefers-color-scheme`. Preferences stay local; no telemetry is sent. ## Connection state -The footer polls `/api/health` and shows one of: +The interface communicates with `/api/health` and displays: -- **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. +- **Connecting:** initial connection in progress. +- **Local service online:** daemon is responding. +- **Service unavailable:** connection to daemon failed; interface remains accessible for offline settings. ## 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 `` 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. +Posting a directory path to `/api/analyze` triggers the Zeek ingestion and +analytical detection pipeline. diff --git a/docs/pt-BR/arquitetura.md b/docs/pt-BR/arquitetura.md index 424baa9..4312815 100644 --- a/docs/pt-BR/arquitetura.md +++ b/docs/pt-BR/arquitetura.md @@ -2,176 +2,161 @@ [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. +O Acauã é um produto único em Rust: um daemon/CLI, um núcleo compartilhado e um +dashboard web embutido. 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ê | +| Camada | Escolha | Motivo | |---|---|---| -| 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 | +| Core, CLI e daemon | Rust 2021 (MSRV 1.88) | Toolchain única, segurança de memória, sem dependências externas de runtime | +| API HTTP | Axum + Tokio + Tower HTTP | Superfície assíncrona enxuta, fácil de embutir | +| Dashboard Web | HTML/CSS/JS vanilla | Sem etapa de build de frontend, assets embutidos no binário via `rust-embed` | +| Armazenamento analítico | DuckDB + Parquet | Armazenamento colunar embutido obrigatório, linkado à `libduckdb.so` oficial pré-compilada | +| Implantação | Docker / docker-compose | Daemon de SOC coletivo, usuário não-root sem privilégios, rootfs somente leitura | +| Validação científica | Python apenas 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. +O workspace possui dois crates: `acaua-core` e `acaua-cli`. Ambos utilizam `version.workspace = true`. ## 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 ────────────┘ +Logs Zeek ──► acaua-core::zeek ──► eventos normalizados ──► storage + │ + ├──► pillar1 (beaconing) + ├──► pillar2 (sessões persistentes) + ├──► pillar3 (anomalias de protocolo) + └──► pillar4 (fusão) ──► alertas + evidência + │ +acaua-cli (serve) ──► API axum ──► dashboard web embutido ──────────┘ ``` -- `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. +- `zeek`: parser delimitado e confinamento de caminhos. Nunca segue symlink fora + da raiz do dataset. +- `storage`: motor DuckDB em memória sobre arquivos Parquet particionados por + dia com TTL de sete dias. +- `pillar1`–`pillar4`: funções puras sobre eventos normalizados. Retornam scores + e evidência, nunca apenas um float. +- `fusion`: combinação limitada em ordem fixa. Deve ser simétrica e reproduzível. +- `doctor`: checagens do ambiente local; reporta linkagem da DuckDB pré-compilada + e status do Zeek em container. +- `update`: checa novas versões consultando a API de Releases do Gitea. ## 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` +Timestamps de eventos são um processo pontual unidimensional: não há valor `y` para ajustar. O periodograma primário é, portanto, Rayleigh/Schuster: ```text -P(f) = |sum_k exp(2*pi*i*f*t_k)|^2 / N +P(T) = (2 / N) * [ (sum cos(2*pi*t_i / T))^2 + (sum sin(2*pi*t_i / T))^2 ] ``` -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. +Lomb–Scargle generalizado fica reservado para a extensão futura que avaliará um +atributo real de resposta (como tamanho de payload ou duração) contra o tempo. -### 2. Bowley com sentinela explícito +### 2. Proteger o caso de dispersão zero no MADM -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. +Um beacon sintético ou sem jitter produz intervalos perfeitamente iguais. O +desvio absoluto mediano em relação à mediana (MADM) é então `0.0`. Dividir por +`0.0` ou tratá-lo como dado ausente é incorreto: dispersão zero é o resultado mais +suspeito possível. A implementação protege esse caso com atribuição explícita de +score (`1.0`) e sentinela documentado. -### 3. Jensen–Shannon, não KL puro +### 3. Substituir assimetria de Pearson por assimetria de Bowley -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. +A assimetria amostral baseada no terceiro momento central é sensível a outliers +individuais e pode divergir. A assimetria de Bowley utiliza quartis: -### 4. Sessões persistentes têm detector próprio +```text +B = (Q3 + Q1 - 2*Q2) / (Q3 - Q1) +``` -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. +Ela é limitada em `[-1.0, 1.0]` por definição e resistente a outliers extremos. +Quando `Q3 == Q1` (dados concentrados), a implementação atribui `0.0` com flag +sentinela. -### 5. Cautela com a licença JA4 +### 4. Isolar o espaço de pesquisa Python -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. +`research/` tem seu próprio ambiente virtual e dependências. Ele é excluído do +workspace Cargo, ignorado em builds Docker e releases de CI, e testado apenas +quando scripts de validação de pesquisa forem invocados diretamente. -### 6. Determinismo e evidência JSON por alerta +### 5. Modo degradado documentado para NetFlow/IPFIX -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. +NetFlow v9 e IPFIX não carregam nomes de consulta DNS, SNI TLS, cadeias de +certificados ou cabeçalhos HTTP. Qualquer análise que dependa de recursos de +protocolo fica indisponível. O Acauã documenta isso como modo degradado e nunca +alega paridade entre registros de fluxo e logs Zeek completos. -## Fluxo de dados +### 6. Fronteira única HTTP e RPC -1. `acaua analyze ` 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. +O daemon expõe uma API Axum em loopback por padrão. A UI web embutida se +comunica com o daemon exclusivamente por chamadas HTTP padrão. ## API -| Método | Caminho | Finalidade | +A API HTTP é servida por `acaua serve`: + +| Método | Caminho | Significado | |---|---|---| | GET | `/api/health` | Liveness e versão | -| GET | `/api/status` | Uptime, diretório de dados, estado dos pilares | +| GET | `/api/status` | Uptime, diretório de dados, sumário do dataset | | GET | `/api/alerts` | Alertas paginados com evidência | -| POST | `/api/analyze` | Analisar um caminho de dataset | -| POST | `/api/update` | Checar atualização da CLI | +| POST | `/api/analyze` | Ingestão e análise de diretório de dataset | +| GET | `/api/update` | Checagem de novas releases | | GET | `/logo.png` | Asset de marca embutido | -| GET | `/*` | Fallback da UI embutida | +| GET | `/*` | Fallback de UI embutida (`rust-embed`) | -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. +`serve` escuta em `127.0.0.1:8787` por padrão. Expô-lo em uma rede externa exige +um 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. +DuckDB é uma dependência obrigatória linkada dinamicamente à `libduckdb.so` +oficial pré-compilada (versão 1.5.6 fixada com SHA-256 em `scripts/fetch-libduckdb.sh`). +Extensões Parquet, JSON e ICU já vêm compiladas estaticamente na biblioteca oficial. +O esquema consiste em arquivos Parquet particionados por dia sob o diretório de +dados configurado, com consultas DuckDB em memória e TTL de sete dias. ## 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. +Atualizações do Acauã são distribuídas por imagens de container (`docker compose pull && docker compose up -d`). +O comando `acaua update` consulta a API de Releases do Gitea para `ciberfauna/acaua` e +notifica os operadores quando uma tag de versão mais nova estiver disponível. A +sobrescrita binária em disco (`update --apply`) foi removida: em um daemon de SOC +coletivo orquestrado via container, atualizações devem ser controladas por puxar +imagens novas em vez de processos que alteram o próprio executável. ## 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/`. +1. **Serviço Python com FastAPI/ClickHouse:** Rejeitado. Um runtime Python + introduziria imagens pesadas e fronteiras de memória desnecessárias; o + ClickHouse adicionaria um servidor externo e custo operacional para datasets + que cabem perfeitamente em um armazenamento colunar embutido. O produto é 100% Rust. +2. **Aplicativo desktop (Tauri / AppImage / NSIS):** Rejeitado. Diferente do Guará + (forense em disco bruto local) e do Sabiá (hardware Wi-Fi em modo monitor), o + Acauã não tem dependência de hardware local: é um daemon de SOC multiusuário + consumindo logs do Zeek. Um wrapper desktop traria manutenção e superfície de + segurança sem ganho operacional. ## 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). +- **Docker Compose (Oficial):** Imagem multi-arch (`git.lucasrguerra.dev.br/ciberfauna/acaua`), + rodando com usuário não-privilegiado `acaua` (UID 10001), filesystem somente leitura e capabilities revogadas. +- **Compilação nativa:** `scripts/fetch-libduckdb.sh ~/.cache/acaua/duckdb` seguido de `cargo build --release -p acaua-cli`. -## Comportamento em falha +## Comportamento em falhas -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. +Entradas malformadas são ignoradas com diagnóstico delimitado e contado. Um log +ausente não é fatal. Um banco corrompido é reportado pelo `doctor` e nunca +recriado silenciosamente sobre dados existentes. ## 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. +SemVer 2.0.0. O JSON de evidência carrega uma versão de esquema. Qualquer +alteração em uma pontuação ou no formato de evidência é breaking change para +versões minor `0.x` e deve constar no changelog. diff --git a/docs/pt-BR/cli.md b/docs/pt-BR/cli.md index 2b77fad..6e0db9f 100644 --- a/docs/pt-BR/cli.md +++ b/docs/pt-BR/cli.md @@ -2,23 +2,22 @@ [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. +O binário `acaua` atua tanto como utilitário de linha de comando quanto como daemon de segundo plano. +Esta página descreve os comandos disponíveis no motor. ```text -acaua [opções] +acaua [options] 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 + serve Inicia a API e o dashboard web embutido + analyze Analisa um diretório de logs do Zeek + doctor Verifica o ambiente local + update Checa por releases disponíveis ``` ## `acaua serve` -Sobe a API HTTP e a UI embutida. +Inicia a API HTTP e o dashboard web embutido. ```bash acaua serve --bind 127.0.0.1:8787 --data-dir .acaua @@ -27,10 +26,10 @@ 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 | +| `--data-dir` | `.acaua` | Diretório de dados e arquivos de execução | -O bind padrão é só loopback. Expor o daemon na rede exige proxy reverso com -autenticação. A API está documentada em +O bind padrão restringe-se a loopback. Expô-lo em uma rede exige um proxy +reverso com autenticação. A API está documentada em [arquitetura.md](arquitetura.md#api). ## `acaua analyze` @@ -40,14 +39,8 @@ 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. +Valida o diretório alvo, ingere tráfego do `conn.log` em partições Parquet e +executa os algoritmos de detecção de ameaças. ## `acaua doctor` @@ -56,51 +49,34 @@ 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. +Executa checagens de ambiente: +- `zeek`: verifica `zeek --version` ou detecta execução em container (`ACAUA_RUNTIME=container`). +- `curl`: confirma disponibilidade de download HTTP. +- `duckdb`: confirma linkagem dinâmica contra a `libduckdb.so` em tempo de execução. +- `research`: valida o isolamento do workspace de pesquisa. ## `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. +Consulta a API de Releases do Gitea, compara a última tag com a versão em +execução e imprime instruções de atualização (`docker compose pull && docker compose up -d` +quando em container). ## 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) | +| `1` | Erro em tempo de execução (caminho inválido, erro de consulta, I/O) | +| `2` | Uso inválido de 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. +| Variável | Significado | +|---|---| +| `ACAUA_RUNTIME` | Definida como `container` em imagens Docker para adaptar checagens | +| `DUCKDB_LIB_DIR` | Caminho do diretório contendo `libduckdb.so` em compilação nativa | +| `LD_LIBRARY_PATH` | Caminho de busca de bibliotecas compartilhadas para `libduckdb.so` | diff --git a/docs/pt-BR/docker.md b/docs/pt-BR/docker.md index 450f0da..8879f8f 100644 --- a/docs/pt-BR/docker.md +++ b/docs/pt-BR/docker.md @@ -2,65 +2,79 @@ [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. +O Acauã é distribuído como uma imagem de container oficial: +`git.lucasrguerra.dev.br/ciberfauna/acaua`. -## Build +Tags de imagem disponíveis: +- `latest`: Release estável mais recente. +- `X.Y.Z` / `X.Y`: Versões semânticas fixadas. +- `edge`: Gerada automaticamente a partir do branch `main`. + +O container executa como usuário de sistema não privilegiado (`acaua`, UID 10001), +com filesystem raiz somente leitura (`read_only: true`), todas as capabilities revogadas +(`cap_drop: [ALL]`), e endurecimento de segurança (`no-new-privileges:true`). + +## Implantação com Compose + +A implantação recomendada para produção e laboratório utiliza Docker Compose: ```bash -docker build -t acaua . -docker run --rm -p 127.0.0.1:8787:8787 -v "$PWD/data/acaua:/data/acaua" acaua +cp .env.example .env # configure ZEEK_INTERFACE (apenas redes autorizadas) +docker compose pull && docker compose up -d ``` -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 +Para compilar e rodar localmente a partir do código: ```bash -cp .env.example .env # opcional: defina ZEEK_INTERFACE -docker compose up --build +docker compose up -d --build ``` -O Compose sobe dois serviços: +A stack coordena 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` | +| Serviço | Imagem | Finalidade | Perfil de segurança | +|---|---|---|---| +| `zeek` | `zeek/zeek:9.0.1` | Captura de pacotes de rede e rotação horária de logs | Rede host, capabilities `NET_RAW` e `NET_ADMIN` | +| `acaua` | `git.lucasrguerra.dev.br/ciberfauna/acaua` | Processamento analítico, API e dashboard web | Não privilegiado (UID 10001), rootfs read-only, sem capabilities | -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. +## Volumes e organização de dados -## Layout de dados +A stack utiliza volumes nomeados para isolamento limpo de permissões e propriedade: -```text -data/ - zeek/ # logs Zeek, escritos pelo serviço zeek - acaua/ # store, exportações e arquivos de runtime +- `zeek-logs`: Volume compartilhado montado em `/logs` no serviço Zeek e montado + como **somente leitura** em `/data/zeek:ro` no serviço Acauã. +- `acaua-data`: Volume dedicado de leitura e escrita montado em `/data/acaua` para o + armazenamento DuckDB, partições Parquet e arquivos de execução. + +Para inspecionar ou extrair dados no host: + +```bash +docker compose cp acaua:/data/acaua ./local-acaua-backup ``` -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. +Ou executar um container efêmero de inspeção: + +```bash +docker run --rm -v acaua_acaua-data:/data:ro debian:trixie-slim ls -la /data +``` + +## Atualização + +Por rodar como um daemon de SOC de uso coletivo, atualizações são realizadas +puxando a imagem verificada mais recente: + +```bash +docker compose pull && docker compose up -d +``` + +O daemon em execução finaliza de forma limpa em resposta ao `SIGTERM` em tempo +sub-segundo, garantindo que gravações Parquet e transações DuckDB sejam concluídas +sem corrupção. ## 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. +- O serviço `zeek` requer privilégios de captura de pacotes (`NET_RAW`, `NET_ADMIN`). + Execute-o apenas em interfaces e redes que você tem autorização explícita para monitorar. +- A publicação padrão do dashboard é restrita a loopback (`ACAUA_PUBLISH=127.0.0.1:8787`). + Não exponha o serviço em interfaces externas sem um proxy reverso com autenticação. +- A imagem não possui telemetria. Tráfego de saída restringe-se a checagens de + versão iniciadas pelo operador contra a API de Releases do Gitea. diff --git a/docs/pt-BR/interface.md b/docs/pt-BR/interface.md index 8c47717..c2a977f 100644 --- a/docs/pt-BR/interface.md +++ b/docs/pt-BR/interface.md @@ -2,71 +2,39 @@ [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. +A interface é um dashboard vanilla em HTML/CSS/JS embutido diretamente no binário +`acaua` via `rust-embed` e servido por `acaua serve`. Não há etapa de build de +frontend nem frameworks pesados em runtime. -## Layout +## Estrutura -- **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. +- **Barra lateral:** logo da marca, navegação, indicador de conexão ao daemon, versão e checagem de atualizações. +- **Visão geral:** aviso de escopo de monitoramento, alerta de uso autorizado e cartões dos quatro pilares de detecção. +- **Análise:** campo de caminho de dataset, botão de análise, atualização e inspeção de saída JSON. +- **Alertas:** listagem de alertas com metadados de evidência determinística. +- **Configurações:** preferências de idioma e tema, salvas no armazenamento local do navegador. ## 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. +O texto original é em português. `i18n.js` aplica as traduções em inglês de +`i18n-en.js` em tempo de execução e salva a escolha no `localStorage`. `Auto` +detecta o idioma do navegador. O teste automatizado `node scripts/check-ui-i18n.mjs` +garante que toda string visível em português tenha seu par exato 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. +Modos claro, escuro e `Auto` são suportados. `Auto` responde ao `prefers-color-scheme` +do sistema operacional. Preferências ficam no host; nenhuma telemetria é emitida. -## Estado da conexão +## Estado de conexão -O rodapé consulta `/api/health` e mostra um de: +A interface comunica-se com `/api/health` e exibe: -- **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. +- **Conectando:** conexão inicial em andamento. +- **Serviço local ativo:** o daemon está respondendo. +- **Serviço indisponível:** falha na conexão com o daemon; a interface continua acessível para ajustes locais. ## 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 `` 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. +Enviar um caminho de diretório para `/api/analyze` dispara a ingestão do Zeek e o +pipeline analítico de detecção. diff --git a/docs/pt-BR/suporte.md b/docs/pt-BR/suporte.md index bd3d84e..69ca158 100644 --- a/docs/pt-BR/suporte.md +++ b/docs/pt-BR/suporte.md @@ -2,46 +2,36 @@ [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. +O Acauã é distribuído como um motor de NDR containerizado. A forma mais rápida de +obter assistência é abrir uma issue no repositório do Gitea. ## Antes de abrir uma issue -1. Rode `acaua doctor --json` e guarde a saída. +1. Execute `acaua doctor --json` dentro do container (`docker exec acaua acaua doctor --json`) ou no host. 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). +3. Tente reproduzir o problema com a menor captura de logs Zeek possível. +4. Revise 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`. +- Sistema operacional do host, arquitetura (`uname -m`) e versão/tag do Acauã. +- O comando exato ou definição do Docker Compose. +- Logs completos do container (`docker compose logs acaua`). +- Amostras anonimizadas de logs Zeek que demonstrem a falha. -Não anexe capturas de produção nem credenciais. Redija endereços, hostnames e -payloads antes de compartilhar. +Não anexe capturas de tráfego de produção nem credenciais. Anonimize endereços IP, +hostnames e payloads sensíveis antes de compartilhar. -## Problemas de segurança +## Questões 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. +Não inclua detalhes de exploração em uma issue pública. Siga a política em +[SECURITY.pt-BR.md](../../SECURITY.pt-BR.md) e solicite um canal privado de comunicação. ## Situações comuns -| Sintoma | O que checar | +| Sintoma | O que verificar | |---|---| -| `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. +| Container não sobe | Confirme linkagem de `libduckdb.so` via `docker logs` e verifique permissões nos volumes nomeados | +| `serve` inacessível a partir de outro host | O serviço escuta em loopback (`127.0.0.1:8787`) por padrão; verifique o proxy reverso | +| `update` falha | Verifique conectividade de rede com a API de Releases do Gitea ou permissões de pull do registry | +| UI web exibe português | `Auto` segue o idioma do navegador; escolha inglês nas configurações | diff --git a/docs/support.md b/docs/support.md index 05644e6..26b9577 100644 --- a/docs/support.md +++ b/docs/support.md @@ -2,46 +2,36 @@ **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. +Acauã is distributed as a containerized NDR engine. The fastest way to get help +is opening an issue in the Gitea repository. ## Before opening an issue -1. Run `acaua doctor --json` and keep the output. +1. Run `acaua doctor --json` inside the container (`docker exec acaua acaua doctor --json`) or host. 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). +3. Try reproducing with the smallest possible Zeek log capture. +4. Review 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`. +- Host operating system, architecture (`uname -m`) and Acauã version/tag. +- The exact command or Docker Compose definition. +- Full container logs (`docker compose logs acaua`). +- Redacted Zeek log samples demonstrating the issue. -Do not attach production captures or credentials. Redact addresses, hostnames -and payloads before sharing. +Do not attach production captures or credentials. Redact IP addresses, hostnames +and sensitive 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. +[SECURITY.md](../SECURITY.md) and request a private communication channel. ## 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. +| Container fails to start | Confirm `libduckdb.so` linkage via `docker logs` and verify permissions on named volumes | +| `serve` is unreachable from another host | Service binds to loopback (`127.0.0.1:8787`) by default; verify reverse proxy configuration | +| `update` fails | Check network connectivity to Gitea Releases API or verify registry pull permissions | +| Web UI shows Portuguese | `Auto` follows the browser language; select English in settings | diff --git a/scripts/build-gui.sh b/scripts/build-gui.sh deleted file mode 100755 index 19ab937..0000000 --- a/scripts/build-gui.sh +++ /dev/null @@ -1,185 +0,0 @@ -#!/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 } -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 -#include -#include - -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 diff --git a/scripts/check.sh b/scripts/check.sh index 2aa2095..0a33830 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -17,6 +17,10 @@ step() { # step "name" command... if "$@"; then printf '\033[32mok\033[0m %s\n' "$name"; else printf '\033[31mFAILED\033[0m %s\n' "$name"; failures+=("$name"); fi } +duckdb_dir="${XDG_CACHE_HOME:-$HOME/.cache}/acaua/duckdb" +scripts/fetch-libduckdb.sh "$duckdb_dir" >/dev/null || { echo "could not install libduckdb" >&2; exit 1; } +export DUCKDB_LIB_DIR="$duckdb_dir" DUCKDB_INCLUDE_DIR="$duckdb_dir" LD_LIBRARY_PATH="$duckdb_dir${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + 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 @@ -32,9 +36,16 @@ terms = [ 'updater' + '.pubkey', 'createUpdater' + 'Artifacts', 'plugin' + '-updater', + 'Tau' + 'ri', + 'App' + 'Image', ] -skip_dirs = {'.git', 'target', '.wolf', '.claude', 'dist-gui'} -allowed = {Path('docs/architecture.md')} +skip_dirs = {'.git', 'target', '.wolf', '.claude'} +allowed = { + Path('docs/architecture.md'), + Path('docs/pt-BR/arquitetura.md'), + Path('CHANGELOG.md'), + Path('CHANGELOG.pt-BR.md'), +} hits = [] for path in Path('.').rglob('*'): if not path.is_file() or any(part in skip_dirs for part in path.parts): @@ -61,9 +72,9 @@ 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 "brand assets" test -s logo.png step "consistent Semantic Versioning 2.0.0" python3 -c " -import json, re, sys +import re, sys from pathlib import Path semver_re = re.compile( @@ -78,22 +89,33 @@ def cargo_version(path): 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]) +version = cargo_version('Cargo.toml') +if not semver_re.match(version): + sys.exit(f'invalid SemVer 2.0.0 version: {version}') +print(version) " + +step "DuckDB pin matches the duckdb crate" python3 -c " +import re, sys +from pathlib import Path +lock = Path('Cargo.lock').read_text() +m = re.search(r'^name = \"duckdb\"\nversion = \"1\.(\d+)\.\d+\"', lock, re.M) +if not m: + sys.exit('duckdb crate not found in Cargo.lock') +enc = int(m.group(1)) +crate = f'{enc // 10000}.{enc // 100 % 100}.{enc % 100}' +pin = re.search(r'^DUCKDB_VERSION=(\S+)$', Path('scripts/fetch-libduckdb.sh').read_text(), re.M).group(1) +if crate != pin: + sys.exit(f'DuckDB mismatch: duckdb crate targets {crate}, scripts/fetch-libduckdb.sh pins {pin}') +print(f'DuckDB {pin}') +" + 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 + +if command -v docker >/dev/null && docker compose version >/dev/null 2>&1; then + step "docker compose config" docker compose config --quiet else - printf '\n(desktop app: skipped, WebKitGTK development libraries not installed; built in CI)\n' + printf '\n(docker compose config: skipped, docker compose not available)\n' fi if [ "$full" = 1 ]; then diff --git a/scripts/fetch-libduckdb.sh b/scripts/fetch-libduckdb.sh new file mode 100755 index 0000000..7c9309c --- /dev/null +++ b/scripts/fetch-libduckdb.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Installs the official prebuilt DuckDB shared library (libduckdb.so and +# duckdb.h) for the current CPU architecture into , after checking +# its SHA-256. DuckDB is never compiled from source in this project. +# +# scripts/fetch-libduckdb.sh +# +# Then build with DUCKDB_LIB_DIR= and run tests or the binary with +# LD_LIBRARY_PATH= (the Docker image uses ldconfig instead). +# +# The pinned version MUST match the duckdb crate in Cargo.lock: duckdb-rs +# encodes the DuckDB version in its second component (DuckDB 1.5.6 <-> duckdb +# 1.10506.x); scripts/check.sh enforces this. To upgrade, change the version +# and both checksums together (from the "digest" field of each asset in +# https://api.github.com/repos/duckdb/duckdb/releases/tags/v). +# DuckDB 2.x renamed these assets to duckdb-shared-libs-linux-.tar.gz. +set -euo pipefail + +DUCKDB_VERSION=1.5.6 +dest=${1:?usage: fetch-libduckdb.sh } + +case "$(uname -m)" in + x86_64 | amd64) arch=amd64 sha256=b845005f5132a7d8180057c35e14a7626632258782f871a90861b19c1c03841b ;; + aarch64 | arm64) arch=arm64 sha256=b72ed9f05003f5e9d2015f7ceada6416b377d9dd33169cdde3c9e33897856eee ;; + *) echo "fetch-libduckdb: unsupported architecture: $(uname -m)" >&2; exit 1 ;; +esac +stamp="$DUCKDB_VERSION-linux-$arch" + +installed() { [ -f "$dest/libduckdb.so" ] && [ "$(cat "$dest/.duckdb-release" 2>/dev/null)" = "$stamp" ]; } +if installed; then + echo "libduckdb $stamp already in $dest" + exit 0 +fi + +command -v unzip >/dev/null || { echo "fetch-libduckdb: unzip is required" >&2; exit 1; } +parent=$(dirname "$dest") +mkdir -p "$parent" +# Stage next to the destination so the final rename is atomic: parallel CI +# jobs sharing /opt/hostedtoolcache never see a half-extracted library. +stage=$(mktemp -d "$parent/.libduckdb.XXXXXX") +trap 'rm -rf "$stage"' EXIT +asset="libduckdb-linux-$arch.zip" +curl -fsSL --retry 3 -o "$stage/$asset" \ + "https://github.com/duckdb/duckdb/releases/download/v$DUCKDB_VERSION/$asset" +echo "$sha256 $stage/$asset" | sha256sum -c --quiet - \ + || { echo "fetch-libduckdb: checksum mismatch for $asset" >&2; exit 1; } +# The archive also carries libduckdb_static.a (~80 MB) and duckdb.hpp; only the +# shared library and the C header are needed. +unzip -o -j -q "$stage/$asset" libduckdb.so duckdb.h -d "$stage/lib" +echo "$stamp" > "$stage/lib/.duckdb-release" +if ! mv -T "$stage/lib" "$dest" 2>/dev/null; then + # Either another job just installed the same release, or an older one is there. + installed || { rm -rf "$dest" && mv -T "$stage/lib" "$dest"; } +fi +echo "libduckdb $stamp installed in $dest" diff --git a/scripts/generate-logo.py b/scripts/generate-logo.py deleted file mode 100755 index 40fa6e2..0000000 --- a/scripts/generate-logo.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/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() diff --git a/scripts/package-cli.sh b/scripts/package-cli.sh deleted file mode 100755 index e99904b..0000000 --- a/scripts/package-cli.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash -# Builds the CLI for a target platform and packages it as expected by the updater: -# acaua--.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 } -platform=${2:?usage: package-cli.sh } -target=${3:?usage: package-cli.sh } - -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" diff --git a/scripts/release-upload.sh b/scripts/release-upload.sh deleted file mode 100755 index 2a24cc2..0000000 --- a/scripts/release-upload.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/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 ...} -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 diff --git a/scripts/smoke-image.sh b/scripts/smoke-image.sh new file mode 100755 index 0000000..d6dd2c6 --- /dev/null +++ b/scripts/smoke-image.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Smoke-tests an Acauã image: the API and the dashboard answer, libduckdb is +# linked, a HEALTHCHECK is declared and SIGTERM stops the daemon cleanly. +# Everything runs through `docker exec`, so no port has to be reachable from +# the CI job container. The container is always removed. +# +# scripts/smoke-image.sh +set -euo pipefail + +image=${1:?usage: smoke-image.sh } +name="acaua-smoke-$$-$RANDOM" +trap 'docker rm -f "$name" >/dev/null 2>&1 || true' EXIT +fail() { echo "smoke: $*" >&2; docker logs "$name" >&2 2>&1 || true; exit 1; } + +docker run -d --name "$name" "$image" >/dev/null +for _ in $(seq 1 30); do + docker exec "$name" curl -fsS http://127.0.0.1:8787/api/health >/dev/null 2>&1 && break + sleep 1 +done +docker exec "$name" curl -fsS http://127.0.0.1:8787/api/health | grep -q '"status":"ok"' || fail "/api/health did not answer" +docker exec "$name" curl -fsS http://127.0.0.1:8787/ | grep -qi '/dev/null +code=$(docker inspect -f '{{.State.ExitCode}}' "$name") +[ "$code" = 0 ] || fail "exit code $code after SIGTERM (expected 0: SIGTERM must stop serve)" +echo "smoke: $image ok"