refactor: distribute Acaua as a Docker image instead of a desktop app
Release / Plan (reuse the published image?) (push) Successful in 3s
CI / Docker image (build and smoke test) (push) Skipped
CI / Format and clippy (push) Successful in 4m32s
CI / Dependency audit (push) Successful in 11s
CI / Tests (Linux) (push) Successful in 9m15s
Release / ci (push) Successful in 9m18s
Release / Build image (arm64) (push) Failing after 7m7s
Release / Build image (amd64) (push) Failing after 13m53s
Release / Publish tags (push) Skipped
Release / Gitea release (push) Skipped
Acaua is a collective SOC daemon consuming only Zeek log files, not a single-user tool needing local hardware like its siblings. Drop the Tauri desktop shell, installers and self-update entirely; the server and its web dashboard ship as a container, updated by pulling a new image tag. Link against the official prebuilt libduckdb instead of compiling it from source via the bundled feature, cutting image build time from 15+ minutes to about 3. Fix a real bug found along the way: as PID 1 in a container, the daemon ignored SIGTERM and was always killed after the stop timeout; it now shuts down gracefully. Also corrects the Pillar 1 beaconing score: the period estimate had a harmonic alias (a perfectly periodic 4h series resolved to 600s), and the old score formula flagged pure random traffic as suspicious (p95 = 0.839 on Poisson noise). The new formula was validated against 300 samples per scenario. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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-<hash> 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/*
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 452 KiB |
@@ -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::Value, String> {
|
||||
serde_json::to_value(doctor::run()).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn analyze_dataset(path: String) -> Result<serde_json::Value, String> {
|
||||
let report = zeek::analyze_dir(&path).map_err(|error| error.to_string())?;
|
||||
serde_json::to_value(report).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn list_alerts() -> serde_json::Value {
|
||||
serde_json::json!([])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_check() -> Result<serde_json::Value, String> {
|
||||
let status = update::check(env!("CARGO_PKG_VERSION")).map_err(|error| error.to_string())?;
|
||||
serde_json::to_value(status).map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn update_apply(app: tauri::AppHandle) -> Result<String, String> {
|
||||
use tauri::Emitter;
|
||||
let exe = std::env::var_os("APPIMAGE")
|
||||
.map(std::path::PathBuf::from)
|
||||
.filter(|p| p.is_file())
|
||||
.or_else(|| std::env::current_exe().ok())
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("acaua-gui"));
|
||||
update::apply_desktop_with_progress(&exe, move |phase| {
|
||||
let _ = app.emit("update-progress", serde_json::json!({ "phase": phase }));
|
||||
})
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
app_info,
|
||||
doctor_report,
|
||||
analyze_dataset,
|
||||
list_alerts,
|
||||
update_check,
|
||||
update_apply
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run Acauã");
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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,12 +86,7 @@ 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 {
|
||||
Command::Update => {
|
||||
let status = update::check(env!("CARGO_PKG_VERSION"))?;
|
||||
println!(
|
||||
"versão atual: {} | última: {}",
|
||||
@@ -100,6 +101,12 @@ async fn run(cli: Cli) -> acaua_core::Result<()> {
|
||||
"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(())
|
||||
}
|
||||
|
||||
@@ -75,9 +75,12 @@ async fn alerts() -> Json<Vec<Alert>> {
|
||||
}
|
||||
|
||||
async fn analyze(
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<AnalyzeRequest>,
|
||||
) -> std::result::Result<Json<AnalysisReport>, (StatusCode, Json<ErrorBody>)> {
|
||||
zeek::analyze_dir(&request.path).map(Json).map_err(|error| {
|
||||
zeek::analyze_dir(&request.path, state.storage.root())
|
||||
.map(Json)
|
||||
.map_err(|error| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(ErrorBody {
|
||||
@@ -100,15 +103,6 @@ async fn update_check() -> std::result::Result<Json<UpdateStatus>, (StatusCode,
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_apply() -> (StatusCode, Json<ErrorBody>) {
|
||||
(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
Json(ErrorBody {
|
||||
error: "update --apply is only available through the acaua CLI".to_string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn logo() -> impl IntoResponse {
|
||||
([(header::CONTENT_TYPE, "image/png")], Body::from(LOGO))
|
||||
}
|
||||
@@ -129,21 +123,38 @@ async fn asset(uri: Uri) -> Response {
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
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)
|
||||
|
||||
@@ -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"
|
||||
@@ -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: if name == "zeek" { "zeek" } else { "runtime" },
|
||||
name: "zeek",
|
||||
ok: false,
|
||||
detail: format!("{name} returned an error"),
|
||||
detail: "zeek returned an error".to_string(),
|
||||
},
|
||||
Err(error) => Check {
|
||||
name: if name == "zeek" { "zeek" } else { "runtime" },
|
||||
name: "zeek",
|
||||
ok: false,
|
||||
detail: format!("{name} not found: {error}"),
|
||||
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: "curl",
|
||||
ok: false,
|
||||
detail: "curl returned an error".to_string(),
|
||||
},
|
||||
Err(error) => Check {
|
||||
name: "curl",
|
||||
ok: false,
|
||||
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,
|
||||
|
||||
@@ -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<T> = std::result::Result<T, AcauaError>;
|
||||
@@ -6,19 +6,370 @@ pub struct BeaconingInput {
|
||||
pub timestamps: Vec<f64>,
|
||||
}
|
||||
|
||||
#[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<BeaconingScore> {
|
||||
Ok(BeaconingScore {
|
||||
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: input.timestamps.len(),
|
||||
method: "rayleigh-schuster-stub",
|
||||
status: "skeleton",
|
||||
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<f64> = 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,
|
||||
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<f64> = (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<f64> = (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);
|
||||
}
|
||||
}
|
||||
@@ -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<f64>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub duration: Option<f64>,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub fn open(root: impl AsRef<Path>) -> Self {
|
||||
Self {
|
||||
root: root.as_ref().to_path_buf(),
|
||||
}
|
||||
pub fn open(root: impl AsRef<Path>) -> Result<Self> {
|
||||
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<duckdb::Connection> {
|
||||
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<DatasetSummary> {
|
||||
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<usize> {
|
||||
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<ConnRecord>, u64, HashSet<String>) {
|
||||
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<String> = 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::<f64>().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<Path>) -> 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<Path>) -> Result<IngestionStats> {
|
||||
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<Vec<TupleTimestamps>> {
|
||||
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<String> = 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());
|
||||
}
|
||||
}
|
||||
@@ -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<Asset>,
|
||||
}
|
||||
|
||||
#[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<String> {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn curl_download(url: &str, destination: &Path) -> Result<()> {
|
||||
let status = Command::new("curl")
|
||||
.args(["-fsSL", "-H", "User-Agent: acaua-updater", "-o"])
|
||||
.arg(destination)
|
||||
.arg(url)
|
||||
.status()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: format!("curl {url}"),
|
||||
detail: "download failed".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn asset_suffix_for_platform() -> String {
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else {
|
||||
"windows"
|
||||
};
|
||||
let arch = if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else {
|
||||
"aarch64"
|
||||
};
|
||||
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
|
||||
format!("-{os}-{arch}.{extension}")
|
||||
}
|
||||
|
||||
pub fn check(current_version: &str) -> Result<UpdateStatus> {
|
||||
let body = match curl_text(LATEST_RELEASE_URL) {
|
||||
Ok(body) => body,
|
||||
@@ -103,492 +55,14 @@ pub fn check(current_version: &str) -> Result<UpdateStatus> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(current_exe: &Path) -> Result<String> {
|
||||
let is_cli = current_exe
|
||||
.file_stem()
|
||||
.map(|stem| stem == "acaua")
|
||||
.unwrap_or(false);
|
||||
if !is_cli {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"automatic updates only replace the acaua CLI; download the desktop installer from {RELEASES_PAGE_URL}"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
let suffix = asset_suffix_for_platform();
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.ends_with(&suffix))
|
||||
.ok_or_else(|| AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"no asset ending in `{suffix}` in release {}",
|
||||
release.tag_name
|
||||
),
|
||||
})?;
|
||||
let checksum_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|candidate| candidate.name == format!("{}.sha256", asset.name));
|
||||
|
||||
let temp_dir = std::env::temp_dir().join("acaua-update");
|
||||
fs::create_dir_all(&temp_dir)?;
|
||||
let archive_path = temp_dir.join(&asset.name);
|
||||
curl_download(&asset.browser_download_url, &archive_path)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
let expected_hash = expected
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let data = fs::read(&archive_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let actual_hash = format!("{:x}", hasher.finalize());
|
||||
if actual_hash != expected_hash {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "SHA-256 checksum mismatch; update discarded".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let extract_dir = temp_dir.join("extracted");
|
||||
let _ = fs::remove_dir_all(&extract_dir);
|
||||
fs::create_dir_all(&extract_dir)?;
|
||||
let status = Command::new("tar")
|
||||
.arg("xf")
|
||||
.arg(&archive_path)
|
||||
.arg("-C")
|
||||
.arg(&extract_dir)
|
||||
.status()
|
||||
.map_err(|error| AcauaError::CommandFailed {
|
||||
cmd: "tar xf".to_string(),
|
||||
detail: error.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "tar xf".to_string(),
|
||||
detail: "extraction failed".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let new_binary = find_binary(&extract_dir)?;
|
||||
let backup = current_exe.with_extension("bak");
|
||||
let _ = fs::remove_file(&backup);
|
||||
fs::rename(current_exe, &backup)?;
|
||||
fs::copy(&new_binary, current_exe)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut permissions = fs::metadata(current_exe)?.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(current_exe, permissions)?;
|
||||
}
|
||||
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
fn find_binary(directory: &Path) -> Result<PathBuf> {
|
||||
fn is_binary(name: &OsStr) -> bool {
|
||||
name == "acaua" || name == "acaua.exe"
|
||||
}
|
||||
|
||||
for entry in fs::read_dir(directory)? {
|
||||
let entry = entry?;
|
||||
if is_binary(&entry.file_name()) {
|
||||
return Ok(entry.path());
|
||||
}
|
||||
if entry.file_type().map(|kind| kind.is_dir()).unwrap_or(false) {
|
||||
for inner in fs::read_dir(entry.path())? {
|
||||
let inner = inner?;
|
||||
if is_binary(&inner.file_name()) {
|
||||
return Ok(inner.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "binary not found in the downloaded package".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Desktop GUI installation type detected at runtime.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DesktopInstall {
|
||||
/// Linux AppImage: real filesystem path of the .AppImage file (from APPIMAGE env var).
|
||||
AppImage(PathBuf),
|
||||
/// Linux Debian package installed via dpkg: `current_exe` is reliable here.
|
||||
Deb,
|
||||
/// Windows NSIS installer: downloaded setup executable run with `/S`
|
||||
/// (silent). Does not relaunch the app on its own — see the comment on
|
||||
/// the `WindowsInstaller` apply path.
|
||||
WindowsInstaller,
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type given an optional `APPIMAGE` environment
|
||||
/// variable value and the current executable path.
|
||||
pub fn detect_desktop_install_from(
|
||||
appimage_env: Option<&std::ffi::OsStr>,
|
||||
current_exe: &Path,
|
||||
) -> Result<DesktopInstall> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Ok(DesktopInstall::WindowsInstaller)
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Some(val) = appimage_env {
|
||||
let path = PathBuf::from(val);
|
||||
if path.is_file() {
|
||||
return Ok(DesktopInstall::AppImage(path));
|
||||
}
|
||||
}
|
||||
if current_exe.is_file()
|
||||
&& current_exe
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|e| e.eq_ignore_ascii_case("appimage"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Ok(DesktopInstall::AppImage(current_exe.to_path_buf()));
|
||||
}
|
||||
Ok(DesktopInstall::Deb)
|
||||
}
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
{
|
||||
let _ = (appimage_env, current_exe);
|
||||
Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"automatic self-update is not supported on this platform; download manually from {RELEASES_PAGE_URL}"
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects the desktop installation type using the live environment.
|
||||
pub fn detect_desktop_install() -> Result<DesktopInstall> {
|
||||
let exe = std::env::current_exe()?;
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), &exe)
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for a desktop installation and CPU architecture.
|
||||
pub fn gui_asset_suffix_for(install: &DesktopInstall, arch: &str) -> Result<&'static str> {
|
||||
match install {
|
||||
DesktopInstall::AppImage(_) => match arch {
|
||||
"x86_64" => Ok("_amd64.AppImage"),
|
||||
"aarch64" => Ok("_aarch64.AppImage"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for AppImage: {other}"),
|
||||
}),
|
||||
},
|
||||
DesktopInstall::Deb => match arch {
|
||||
"x86_64" => Ok("_amd64.deb"),
|
||||
"aarch64" => Ok("_arm64.deb"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for deb package: {other}"),
|
||||
}),
|
||||
},
|
||||
DesktopInstall::WindowsInstaller => match arch {
|
||||
"x86_64" => Ok("_x64-setup.exe"),
|
||||
other => Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("unsupported architecture for Windows installer: {other}"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected release asset suffix for the current architecture.
|
||||
pub fn gui_asset_suffix(install: &DesktopInstall) -> Result<&'static str> {
|
||||
gui_asset_suffix_for(install, std::env::consts::ARCH)
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place.
|
||||
pub fn apply_desktop(current_exe: &Path) -> Result<String> {
|
||||
apply_desktop_with_progress(current_exe, |_| {})
|
||||
}
|
||||
|
||||
/// Downloads, verifies, and installs the latest desktop GUI release in place,
|
||||
/// notifying the progress callback of phase transitions (`"downloading"`,
|
||||
/// `"verifying"`, `"installing"`, `"restarting"`).
|
||||
pub fn apply_desktop_with_progress<F>(current_exe: &Path, on_progress: F) -> Result<String>
|
||||
where
|
||||
F: Fn(&str),
|
||||
{
|
||||
let install =
|
||||
detect_desktop_install_from(std::env::var_os("APPIMAGE").as_deref(), current_exe)?;
|
||||
let body = curl_text(LATEST_RELEASE_URL)?;
|
||||
let release: Release = serde_json::from_str(&body)?;
|
||||
|
||||
let suffix = gui_asset_suffix(&install)?;
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name.ends_with(suffix))
|
||||
.ok_or_else(|| AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!(
|
||||
"no asset ending with `{suffix}` in release {}",
|
||||
release.tag_name
|
||||
),
|
||||
})?;
|
||||
let checksum_asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == format!("{}.sha256", asset.name));
|
||||
|
||||
let tmp_dir = std::env::temp_dir().join("acaua-desktop-update");
|
||||
fs::create_dir_all(&tmp_dir)?;
|
||||
let download_path = tmp_dir.join(&asset.name);
|
||||
|
||||
on_progress("downloading");
|
||||
curl_download(&asset.browser_download_url, &download_path)?;
|
||||
|
||||
if let Some(checksum_asset) = checksum_asset {
|
||||
on_progress("verifying");
|
||||
let expected = curl_text(&checksum_asset.browser_download_url)?;
|
||||
let expected_hash = expected
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let data = fs::read(&download_path)?;
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&data);
|
||||
let actual_hash: String = hasher
|
||||
.finalize()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect();
|
||||
if actual_hash != expected_hash {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "SHA-256 checksum mismatch — update discarded".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
on_progress("installing");
|
||||
match install {
|
||||
DesktopInstall::AppImage(target_appimage) => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let parent_dir = target_appimage.parent().unwrap_or_else(|| Path::new("."));
|
||||
let temp_name = format!(
|
||||
".{}.tmp-{}",
|
||||
target_appimage
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("appimage"),
|
||||
std::process::id()
|
||||
);
|
||||
let temp_file = parent_dir.join(temp_name);
|
||||
fs::copy(&download_path, &temp_file)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = fs::metadata(&temp_file)?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(&temp_file, perms)?;
|
||||
}
|
||||
fs::rename(&temp_file, &target_appimage)?;
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(&target_appimage)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("failed to relaunch AppImage: {err}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&target_appimage, &download_path);
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "AppImage updates are only supported on Linux".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
DesktopInstall::Deb => {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let status = Command::new("dpkg")
|
||||
.args(["-i"])
|
||||
.arg(&download_path)
|
||||
.status()
|
||||
.map_err(|e| AcauaError::CommandFailed {
|
||||
cmd: "dpkg -i".to_string(),
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
if !status.success() {
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "dpkg -i".to_string(),
|
||||
detail: format!("dpkg -i failed with exit code {:?}", status.code()),
|
||||
});
|
||||
}
|
||||
let _ = fs::remove_file(&download_path);
|
||||
|
||||
on_progress("restarting");
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = Command::new(current_exe)
|
||||
.args(std::env::args().skip(1))
|
||||
.exec();
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: format!("failed to relaunch updated app: {err}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = (&download_path, current_exe);
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "deb updates are only supported on Linux".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
DesktopInstall::WindowsInstaller => {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// `/S` is the one NSIS silent-install switch with a documented,
|
||||
// stable meaning. There is no standard NSIS or Tauri flag that
|
||||
// makes a silent install relaunch the app afterwards — that was
|
||||
// claimed in an earlier draft of this code and could not be
|
||||
// verified against NSIS's or Tauri's documentation; passing an
|
||||
// invented flag to a stock NSIS installer is silently ignored,
|
||||
// not harmful, but also not a relaunch mechanism. Spawning
|
||||
// detached and exiting immediately (instead of waiting on the
|
||||
// child) also avoids a self-deadlock: if the installer's script
|
||||
// waits for this process to close before it can overwrite the
|
||||
// running .exe, blocking here on the installer's exit status
|
||||
// would hang forever. The practical effect: the update installs
|
||||
// silently, but the app does not reopen itself — the user needs
|
||||
// to start it again. This needs real-world confirmation on
|
||||
// Windows before relying on it; if testing shows the bundled
|
||||
// NSIS template does support an explicit relaunch flag or step,
|
||||
// wire it in here instead of this comment.
|
||||
on_progress("restarting");
|
||||
Command::new(&download_path)
|
||||
.arg("/S")
|
||||
.spawn()
|
||||
.map_err(|e| AcauaError::CommandFailed {
|
||||
cmd: "installer".to_string(),
|
||||
detail: format!("failed to launch installer: {e}"),
|
||||
})?;
|
||||
std::process::exit(0);
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let _ = &download_path;
|
||||
return Err(AcauaError::CommandFailed {
|
||||
cmd: "update".to_string(),
|
||||
detail: "Windows installer updates are only supported on Windows".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
Ok(release.tag_name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn apply_refuses_to_overwrite_the_desktop_app() {
|
||||
let error = apply(Path::new("/opt/acaua/acaua-gui")).unwrap_err();
|
||||
assert!(error.to_string().contains("CLI"), "{error}");
|
||||
assert!(error.to_string().contains(RELEASES_PAGE_URL), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_desktop_install_variants() {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let temp_appimage =
|
||||
std::env::temp_dir().join(format!("test-acaua-{}.AppImage", std::process::id()));
|
||||
let _ = fs::write(&temp_appimage, b"dummy appimage");
|
||||
|
||||
let res = detect_desktop_install_from(
|
||||
Some(temp_appimage.as_os_str()),
|
||||
Path::new("/usr/bin/acaua-gui"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::AppImage(temp_appimage.clone()));
|
||||
|
||||
let res_fallback = detect_desktop_install_from(None, &temp_appimage).unwrap();
|
||||
assert_eq!(
|
||||
res_fallback,
|
||||
DesktopInstall::AppImage(temp_appimage.clone())
|
||||
);
|
||||
|
||||
let _ = fs::remove_file(&temp_appimage);
|
||||
|
||||
let res_deb =
|
||||
detect_desktop_install_from(None, Path::new("/usr/bin/acaua-gui")).unwrap();
|
||||
assert_eq!(res_deb, DesktopInstall::Deb);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let res = detect_desktop_install_from(
|
||||
None,
|
||||
Path::new("C:/Program Files/Acaua/acaua-gui.exe"),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(res, DesktopInstall::WindowsInstaller);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_asset_suffix_matches_known_release_naming() {
|
||||
let appimage = DesktopInstall::AppImage(PathBuf::from("/tmp/Acaua.AppImage"));
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "x86_64").unwrap(),
|
||||
"_amd64.AppImage"
|
||||
);
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&appimage, "aarch64").unwrap(),
|
||||
"_aarch64.AppImage"
|
||||
);
|
||||
|
||||
let deb = DesktopInstall::Deb;
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "x86_64").unwrap(), "_amd64.deb");
|
||||
assert_eq!(gui_asset_suffix_for(&deb, "aarch64").unwrap(), "_arm64.deb");
|
||||
|
||||
let win = DesktopInstall::WindowsInstaller;
|
||||
assert_eq!(
|
||||
gui_asset_suffix_for(&win, "x86_64").unwrap(),
|
||||
"_x64-setup.exe"
|
||||
);
|
||||
|
||||
assert!(gui_asset_suffix_for(&appimage, "mips").is_err());
|
||||
assert!(gui_asset_suffix_for(&deb, "riscv64").is_err());
|
||||
assert!(gui_asset_suffix_for(&win, "arm").is_err());
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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<Path>) -> Result<AnalysisReport> {
|
||||
pub fn analyze_dir(
|
||||
directory: impl AsRef<Path>,
|
||||
data_dir: impl AsRef<Path>,
|
||||
) -> Result<AnalysisReport> {
|
||||
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<Path>) -> Result<AnalysisReport> {
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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(_)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
@@ -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 <dir>` validates the directory, reads supported logs and
|
||||
normalizes events. In this skeleton it validates and returns a stub report.
|
||||
2. Normalized events are written to the local store (when the `duckdb` feature
|
||||
is enabled) with a seven-day TTL.
|
||||
3. Each pillar runs over a deterministic window and returns candidate scores
|
||||
with evidence.
|
||||
4. Fusion combines candidates, applies thresholds and writes alerts.
|
||||
5. `serve` exposes the API and the embedded UI; the Tauri shell can open the same
|
||||
UI against a local or remote daemon.
|
||||
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
|
||||
|
||||
|
||||
@@ -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 <command> [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 |
|
||||
@@ -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.
|
||||
@@ -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 `<dialog>` with a labelled title.
|
||||
- Status changes use text, not color alone.
|
||||
|
||||
## Limitations
|
||||
|
||||
- No real screenshots: the views are shells.
|
||||
- No alert detail, no charts and no dataset browser yet.
|
||||
- No authentication: the UI assumes a loopback daemon.
|
||||
- The desktop app does not self-update; it only reports an available version.
|
||||
Posting a directory path to `/api/analyze` triggers the Zeek ingestion and
|
||||
analytical detection pipeline.
|
||||
@@ -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
|
||||
Logs Zeek ──► acaua-core::zeek ──► eventos normalizados ──► storage
|
||||
│
|
||||
├──► pilar1 (beaconing)
|
||||
├──► pilar2 (sessões persistentes)
|
||||
├──► pilar3 (anomalias de protocolo)
|
||||
└──► pilar4 (fusão) ──► alertas + evidência
|
||||
├──► pillar1 (beaconing)
|
||||
├──► pillar2 (sessões persistentes)
|
||||
├──► pillar3 (anomalias de protocolo)
|
||||
└──► pillar4 (fusão) ──► alertas + evidência
|
||||
│
|
||||
acaua-cli ──► API axum ──► UI embutida ◄── shell Tauri ────────────┘
|
||||
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 <dir>` valida o diretório, lê logs suportados e normaliza
|
||||
eventos. Neste esqueleto ele valida e devolve um relatório stub.
|
||||
2. Eventos normalizados vão para o armazenamento local (quando a feature
|
||||
`duckdb` está ligada) com TTL de sete dias.
|
||||
3. Cada pilar roda sobre uma janela determinística e devolve candidatos com
|
||||
evidência.
|
||||
4. A fusão combina candidatos, aplica limiares e grava alertas.
|
||||
5. `serve` expõe a API e a UI embutida; o shell Tauri abre a mesma UI contra um
|
||||
daemon local ou remoto.
|
||||
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.
|
||||
@@ -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 <comando> [opções]
|
||||
acaua <command> [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` |
|
||||
@@ -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.
|
||||
@@ -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 `<dialog>` com título rotulado.
|
||||
- Mudanças de estado usam texto, não só cor.
|
||||
|
||||
## Limitações
|
||||
|
||||
- Sem screenshots reais: as telas são shells.
|
||||
- Sem detalhe de alerta, gráficos ou navegador de dataset ainda.
|
||||
- Sem autenticação: a UI assume daemon em loopback.
|
||||
- O app desktop não se auto-atualiza; ele só informa a versão disponível.
|
||||
Enviar um caminho de diretório para `/api/analyze` dispara a ingestão do Zeek e o
|
||||
pipeline analítico de detecção.
|
||||
@@ -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 |
|
||||
@@ -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 |
|
||||
@@ -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 <linux|linux-x86_64|linux-aarch64|windows|windows-docker>}
|
||||
tauri() { npx --yes @tauri-apps/cli@2 "$@"; }
|
||||
|
||||
|
||||
if [ "$platform" = windows-docker ]; then
|
||||
docker volume create acaua-cargo >/dev/null
|
||||
docker volume create acaua-rustup >/dev/null
|
||||
mkdir -p dist-gui
|
||||
exec docker run --rm -v "$PWD":/src -v acaua-cargo:/root/.cargo -v acaua-rustup:/root/.rustup \
|
||||
-w /src -e HOST_UID="$(id -u)" -e HOST_GID="$(id -g)" \
|
||||
ubuntu:24.04 bash -c '
|
||||
set -e
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl ca-certificates build-essential pkg-config nsis lld llvm clang nodejs npm unzip >/dev/null
|
||||
[ -x /root/.cargo/bin/rustup ] || curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal >/dev/null
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
command -v cargo-xwin >/dev/null || cargo install cargo-xwin --locked
|
||||
scripts/build-gui.sh windows
|
||||
chown -R "$HOST_UID:$HOST_GID" dist-gui apps/gui/src-tauri/target 2>/dev/null || true'
|
||||
fi
|
||||
|
||||
rm -rf dist-gui
|
||||
mkdir -p dist-gui
|
||||
cd apps/gui/src-tauri
|
||||
|
||||
# Generate derived icons if missing.
|
||||
[ -f icons/icon.ico ] || tauri icon icons/source.png -o icons
|
||||
|
||||
tauri_blob_cache="$HOME/.cache/tauri"
|
||||
if [ -d /opt/hostedtoolcache ] && [ -w /opt/hostedtoolcache ]; then
|
||||
tauri_blob_cache="/opt/hostedtoolcache/tauri-tools"
|
||||
fi
|
||||
mkdir -p "$tauri_blob_cache" "$HOME/.cache/tauri"
|
||||
|
||||
fetch_cached_tool() {
|
||||
local name=$1 url=$2 patch_elf=${3:-0}
|
||||
local cached="$tauri_blob_cache/$name"
|
||||
if [ ! -s "$cached" ]; then
|
||||
local tmp="$cached.tmp.$$"
|
||||
curl -fsSL "$url" -o "$tmp"
|
||||
if [ "$patch_elf" = 1 ]; then
|
||||
dd if=/dev/zero of="$tmp" bs=1 count=3 seek=8 conv=notrunc 2>/dev/null
|
||||
fi
|
||||
chmod +x "$tmp"
|
||||
mv -f "$tmp" "$cached"
|
||||
fi
|
||||
if [ "$cached" != "$HOME/.cache/tauri/$name" ]; then
|
||||
cp -f "$cached" "$HOME/.cache/tauri/$name"
|
||||
chmod +x "$HOME/.cache/tauri/$name"
|
||||
fi
|
||||
}
|
||||
|
||||
case "$platform" in
|
||||
linux|linux-aarch64)
|
||||
bundle="${CARGO_TARGET_DIR:-target}/release/bundle"
|
||||
rm -rf "$bundle/appimage" "$bundle/deb"
|
||||
tauri build --bundles appimage,deb
|
||||
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
||||
;;
|
||||
linux-x86_64)
|
||||
if [ "$(uname -m)" = x86_64 ]; then
|
||||
bundle="${CARGO_TARGET_DIR:-target}/release/bundle"
|
||||
rm -rf "$bundle/appimage" "$bundle/deb"
|
||||
tauri build --bundles appimage,deb
|
||||
else
|
||||
rustup target add x86_64-unknown-linux-gnu >/dev/null
|
||||
mkdir -p /tmp/x86_64-cross-tools "$HOME/.cache/tauri"
|
||||
cat >/tmp/x86_64-cross-tools/ldd <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
exec qemu-x86_64 /lib64/ld-linux-x86-64.so.2 --list "$@"
|
||||
EOF
|
||||
chmod +x /tmp/x86_64-cross-tools/ldd
|
||||
ln -sf /usr/bin/x86_64-linux-gnu-strip /tmp/x86_64-cross-tools/strip
|
||||
for tool_rel in glib-2.0/gio-querymodules gdk-pixbuf-2.0/gdk-pixbuf-query-loaders libgtk-3-0t64/gtk-query-immodules-3.0 libgtk-3-0/gtk-query-immodules-3.0; do
|
||||
if [ -x "/usr/lib/x86_64-linux-gnu/$tool_rel" ]; then
|
||||
tname=$(basename "$tool_rel")
|
||||
printf '#!/usr/bin/env bash\nexec qemu-x86_64 "/usr/lib/x86_64-linux-gnu/%s" "$@"\n' "$tool_rel" >"/tmp/x86_64-cross-tools/$tname"
|
||||
chmod +x "/tmp/x86_64-cross-tools/$tname"
|
||||
fi
|
||||
done
|
||||
fetch_cached_tool "linuxdeploy-07333c6-aarch64.AppImage" \
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy-07333c6/linuxdeploy-aarch64.AppImage" 1
|
||||
# tauri-bundler looks for this exact cache name (no arch suffix); the
|
||||
# architecture only appears in the download URL.
|
||||
fetch_cached_tool "linuxdeploy-plugin-appimage.AppImage" \
|
||||
"https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-aarch64.AppImage" 1
|
||||
fetch_cached_tool "AppRun-x86_64" \
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-x86_64" 0
|
||||
fetch_cached_tool "runtime-x86_64" \
|
||||
"https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64" 0
|
||||
# tauri-bundler runs `dd ... seek=8` on this path to hide linuxdeploy from
|
||||
# AppImage integration tools. A shell-script wrapper loses its shebang and
|
||||
# then fails with ENOENT, so use a native ELF launcher: the dd writes into
|
||||
# ELF padding and the launcher stays executable.
|
||||
launcher_src=/tmp/x86_64-cross-tools/linuxdeploy-x86_64.c
|
||||
launcher_bin="$HOME/.cache/tauri/linuxdeploy-07333c6-x86_64.AppImage"
|
||||
cat >"$launcher_src" <<'EOF'
|
||||
#define _POSIX_C_SOURCE 200809L
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
const char *home = getenv("HOME");
|
||||
if (home == NULL) { fputs("HOME is not set\n", stderr); return 127; }
|
||||
char target[4096];
|
||||
char runtime[4096];
|
||||
char path[8192];
|
||||
const char *old_path = getenv("PATH");
|
||||
snprintf(target, sizeof target, "%s/.cache/tauri/linuxdeploy-07333c6-aarch64.AppImage", home);
|
||||
snprintf(runtime, sizeof runtime, "%s/.cache/tauri/runtime-x86_64", home);
|
||||
snprintf(path, sizeof path, "/tmp/x86_64-cross-tools:%s",
|
||||
old_path ? old_path : "/usr/local/bin:/usr/bin:/bin");
|
||||
if (setenv("ARCH", "x86_64", 1) ||
|
||||
setenv("NO_STRIP", "true", 1) ||
|
||||
setenv("APPIMAGE_EXTRACT_AND_RUN", "1", 1) ||
|
||||
setenv("LDAI_RUNTIME_FILE", runtime, 1) ||
|
||||
setenv("LD_GTK_LIBRARY_PATH", "/usr/lib/x86_64-linux-gnu", 1) ||
|
||||
setenv("DEB_HOST_MULTIARCH", "x86_64-linux-gnu", 1) ||
|
||||
setenv("PKG_CONFIG_ALLOW_CROSS", "1", 1) ||
|
||||
setenv("PKG_CONFIG_LIBDIR", "/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig", 1) ||
|
||||
setenv("PATH", path, 1)) {
|
||||
perror("setenv");
|
||||
return 127;
|
||||
}
|
||||
char **args = calloc((size_t)argc + 1, sizeof *args);
|
||||
if (args == NULL) { perror("calloc"); return 127; }
|
||||
args[0] = target;
|
||||
for (int i = 1; i < argc; i++) args[i] = argv[i];
|
||||
execv(target, args);
|
||||
perror(target);
|
||||
return 127;
|
||||
}
|
||||
EOF
|
||||
cc -O2 -Wall -Wextra -o "$launcher_bin" "$launcher_src"
|
||||
export PKG_CONFIG_ALLOW_CROSS=1
|
||||
export PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
|
||||
export CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
|
||||
export CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++
|
||||
bundle="${CARGO_TARGET_DIR:-target}/x86_64-unknown-linux-gnu/release/bundle"
|
||||
rm -rf "$bundle/appimage" "$bundle/deb"
|
||||
tauri build --target x86_64-unknown-linux-gnu --bundles appimage,deb
|
||||
fi
|
||||
cp "$bundle"/appimage/*.AppImage "$bundle"/deb/*.deb ../../../dist-gui/
|
||||
;;
|
||||
windows)
|
||||
rustup target add x86_64-pc-windows-msvc >/dev/null
|
||||
bundle="${CARGO_TARGET_DIR:-target}/x86_64-pc-windows-msvc/release/bundle/nsis"
|
||||
rm -rf "$bundle"
|
||||
tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc --bundles nsis
|
||||
cp "$bundle"/*-setup.exe ../../../dist-gui/
|
||||
;;
|
||||
*) echo "unknown platform: $platform" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
(
|
||||
cd ../../../dist-gui
|
||||
for asset in *; do
|
||||
[ -f "$asset" ] || continue
|
||||
case "$asset" in
|
||||
*.sha256) ;;
|
||||
*)
|
||||
sha256sum "$asset" > "$asset.sha256"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
)
|
||||
|
||||
ls -1 ../../../dist-gui
|
||||
@@ -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
|
||||
|
||||
@@ -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 <dest-dir>, after checking
|
||||
# its SHA-256. DuckDB is never compiled from source in this project.
|
||||
#
|
||||
# scripts/fetch-libduckdb.sh <dest-dir>
|
||||
#
|
||||
# Then build with DUCKDB_LIB_DIR=<dest-dir> and run tests or the binary with
|
||||
# LD_LIBRARY_PATH=<dest-dir> (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<version>).
|
||||
# DuckDB 2.x renamed these assets to duckdb-shared-libs-linux-<arch>.tar.gz.
|
||||
set -euo pipefail
|
||||
|
||||
DUCKDB_VERSION=1.5.6
|
||||
dest=${1:?usage: fetch-libduckdb.sh <dest-dir>}
|
||||
|
||||
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"
|
||||
@@ -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()
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the CLI for a target platform and packages it as expected by the updater:
|
||||
# acaua-<tag>-<platform>.tar.gz alongside a .sha256 checksum file.
|
||||
#
|
||||
# scripts/package-cli.sh v1.0.3 linux-x86_64 x86_64-unknown-linux-gnu
|
||||
#
|
||||
# Supported platforms: linux-x86_64 and linux-aarch64. When cross-compiling
|
||||
# from a non-x86_64 host, uses gcc-x86-64-linux-gnu. Writes the archives to the
|
||||
# repository root and prints their filenames.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
tag=${1:?usage: package-cli.sh <tag> <platform> <rust-target>}
|
||||
platform=${2:?usage: package-cli.sh <tag> <platform> <rust-target>}
|
||||
target=${3:?usage: package-cli.sh <tag> <platform> <rust-target>}
|
||||
|
||||
case "$target" in
|
||||
x86_64-unknown-linux-gnu)
|
||||
if [ "$(uname -m)" != x86_64 ]; then
|
||||
export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc
|
||||
export CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc
|
||||
fi ;;
|
||||
esac
|
||||
|
||||
rustup target add "$target" >/dev/null 2>&1 || true
|
||||
cargo build --release --locked -p acaua-cli --target "$target"
|
||||
|
||||
name="acaua-${tag}-${platform}"
|
||||
rm -rf "$name"; mkdir "$name"
|
||||
cp "${CARGO_TARGET_DIR:-target}/${target}/release/acaua" "$name/"
|
||||
cp README.md LICENSE-MIT LICENSE-APACHE "$name/"
|
||||
|
||||
tar czf "$name.tar.gz" "$name"
|
||||
asset="$name.tar.gz"
|
||||
rm -rf "$name"
|
||||
sha256sum "$asset" > "$asset.sha256"
|
||||
echo "$asset"
|
||||
echo "$asset.sha256"
|
||||
@@ -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 <tag> <file>...}
|
||||
shift
|
||||
|
||||
auth=(-H "Authorization: token $TOKEN")
|
||||
id=$(curl -fsS "${auth[@]}" "$API/releases/tags/$tag" | jq -r .id)
|
||||
|
||||
for file in "$@"; do
|
||||
name=$(basename "$file")
|
||||
encoded=$(jq -rn --arg n "$name" '$n|@uri')
|
||||
for old in $(curl -fsS "${auth[@]}" "$API/releases/$id/assets" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id'); do
|
||||
curl -fsS -X DELETE "${auth[@]}" "$API/releases/$id/assets/$old"
|
||||
done
|
||||
curl -fsS -X POST "${auth[@]}" -F "attachment=@$file" "$API/releases/$id/assets?name=$encoded" >/dev/null
|
||||
echo "uploaded: $name"
|
||||
done
|
||||
@@ -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 <image-ref>
|
||||
set -euo pipefail
|
||||
|
||||
image=${1:?usage: smoke-image.sh <image-ref>}
|
||||
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 '<!doctype html' || fail "dashboard is not served"
|
||||
docker exec "$name" acaua doctor --json | grep -A1 '"name": "duckdb"' | grep -q '"ok": true' || fail "libduckdb is not usable"
|
||||
[ "$(docker inspect -f '{{if .Config.Healthcheck}}yes{{end}}' "$name")" = yes ] || fail "image has no HEALTHCHECK"
|
||||
[ "$(docker exec "$name" id -u)" != 0 ] || fail "daemon runs as root"
|
||||
docker stop -t 10 "$name" >/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"
|
||||