From 44f662afc0019f382289f6bf8629f4039ba892bf Mon Sep 17 00:00:00 2001 From: alleyviper <41556507+alleyviper@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:06:02 +0100 Subject: [PATCH] feat(distribution): production install/update/backup scripts + public-repo sync (#106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds the customer-facing distribution: a curated distribution/ directory containing only what an end customer needs (production docker-compose.yml with no build: sections, install.sh/update.sh/uninstall.sh/healthcheck.sh/ backup.sh/restore.sh, a customer-facing CHANGELOG.md, and README/LICENSE/ NOTICE/THIRD_PARTY_LICENSES) — pushed as the initial content of the new public alleyviper/argus repo. install.sh: detects OS, validates Docker/Compose, fetches all deployment files, generates a secure DB password, creates the required Docker network, pulls images, starts the stack, waits for health, and rotates the default admin/admin credentials via the auth API — printing the generated password once at the end. update.sh/healthcheck.sh/backup.sh/restore.sh/uninstall.sh formalize what was previously ad-hoc README snippets into real, safe-by-default scripts (uninstall.sh keeps data unless --remove-data is explicitly passed and confirmed; restore.sh requires typed confirmation and documents that it targets a fresh install, not a live merge). Added a sync-distribution job to release.yml: after a successful release, mirrors distribution/ into the public repo and creates a matching (customer-facing, commit-log-free) release marker there. Needs a one-time setup step — a fine-grained PAT scoped to alleyviper/argus added as the ARGUS_PUBLIC_REPO_TOKEN secret — the job cleanly no-ops until that's added. Not yet done: GHCR package visibility (argus-secure-api/-ui/-nginx) is still private, which blocks a genuine end-to-end curl-install test from a clean, unauthenticated environment — deferred at the user's request until they flip it manually (GitHub does not expose this via API). --- .env.example | 65 +++++++ CHANGELOG.md | 31 +++ LICENSE | 17 ++ NOTICE | 18 ++ README.md | 221 ++++++++++++++++++++++ THIRD_PARTY_LICENSES | 112 +++++++++++ backup.sh | 39 ++++ docker-compose.yml | 229 +++++++++++++++++++++++ docker-socket-proxy/haproxy.cfg.template | 97 ++++++++++ healthcheck.sh | 71 +++++++ install.sh | 144 ++++++++++++++ restore.sh | 53 ++++++ uninstall.sh | 59 ++++++ update.sh | 57 ++++++ 14 files changed, 1213 insertions(+) create mode 100644 .env.example create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.md create mode 100644 THIRD_PARTY_LICENSES create mode 100755 backup.sh create mode 100644 docker-compose.yml create mode 100644 docker-socket-proxy/haproxy.cfg.template create mode 100755 healthcheck.sh create mode 100755 install.sh create mode 100755 restore.sh create mode 100755 uninstall.sh create mode 100755 update.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..62d3878 --- /dev/null +++ b/.env.example @@ -0,0 +1,65 @@ +# ARGUS — Environment Configuration +# Copy this file to .env and modify as needed. install.sh does this for you +# automatically, including generating a secure DB_PASSWORD. + +# =========================================== +# Required Settings +# =========================================== + +# Database password — install.sh generates this for you. +# Manual value: openssl rand -base64 24 +DB_PASSWORD=change-me-in-production + +# =========================================== +# Optional Settings +# =========================================== + +TZ=UTC +DB_USER=postgres +DB_NAME=argus + +# =========================================== +# ANIS — Threat Intelligence Integration +# =========================================== +# ANIS is a separate product, deployed independently (self-hosted or via a +# community instance). These vars configure how THIS ARGUS instance talks to +# an already-running ANIS service — they do not start one. +# +# Set ANIS_ENABLED=true and ANIS_URL to turn on reporting WAF auto-bans to +# ANIS and pulling its community threat-intelligence feed. + +ANIS_ENABLED=false +# ANIS_URL=https://anis.example.com:8090 +ANIS_LICENSE_KEY= +# Must match the ANIS instance's own ANIS_ADMIN_KEY. Generate: openssl rand -hex 32 +ANIS_ADMIN_KEY= +ANIS_SHARE_ATTACKERS=false + +# =========================================== +# Docker Compatibility (Synology DSM, etc.) +# =========================================== + +# Leave empty for auto-detection. Only set if auto-detection fails. +# Synology DSM typically needs: 1.41 or 1.43 +# DOCKER_API_VERSION=1.41 + +# =========================================== +# Network & Port Settings +# =========================================== + +# Admin panel port (default: 81) +# UI_PORT=81 + +# Nginx listen ports — change if 80/443 are already in use +# (e.g. Synology Web Station) +# NGINX_HTTP_PORT=8080 +# NGINX_HTTPS_PORT=8443 + +# API host port — exposed for nginx-to-API communication. +# MUST NOT conflict with NGINX_HTTP_PORT. +# API_HOST_PORT=9080 + +# DNS Security Engine listen address — host-only (127.0.0.1) by default. +# Set to an internal interface IP to route real client DNS traffic through +# it. Never 0.0.0.0 unless this host is on a fully trusted network. +# DNS_LISTEN_HOST=127.0.0.1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..09c4d80 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +All notable changes to ARGUS are documented here, in plain language for operators. +Versioning follows [Semantic Versioning](https://semver.org/). + +## [3.29.0] — 2026-07-19 + +### Security +- Hardened the Challenge Engine's internal signing so a rare startup condition can no + longer weaken bot-challenge verification. +- Updated bundled build tooling to close several third-party dependency advisories. + +## [3.28.0] — 2026-07-19 + +### Added +- DNS Protection settings now include contextual help explaining what each detector and + threshold does, so you can tune it with confidence instead of guessing. + +## [3.27.0] — 2026-07-19 + +### Added +- Complete user-management workflow: edit user details, admin-initiated password resets, + MFA reset, account lock/unlock, session management, and login history — all from one + panel per user. +- Redesigned Attack Origin visualization: a single live global map (previously two separate, + inconsistent views) showing real-time attack traffic with per-event detail on demand. + +## [3.26.0] and earlier + +Earlier release history predates this distribution's changelog. See the GitHub Releases +page for this repository going forward — every release from here on is documented here. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6ad2dd2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +ARGUS Secure — Proprietary Software License +Copyright (c) 2025-2026 AI4SEC / lmelo. All rights reserved. + +** PLACEHOLDER — NOT A FINAL LICENSE ** + +This repository contains proprietary, commercial software. A complete commercial license +agreement has not yet been finalized and this file is a placeholder pending legal review. + +Until a final license is adopted, no rights are granted to use, copy, modify, merge, publish, +distribute, sublicense, or sell copies of this software, in whole or in part, except as +separately agreed in writing with the copyright holder. + +This software incorporates third-party open-source components under their own licenses; +see THIRD_PARTY_LICENSES and NOTICE for details. Nothing in this placeholder alters the terms +of those third-party components. + +TODO before release: replace this file with final commercial/enterprise license text. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..ac771a8 --- /dev/null +++ b/NOTICE @@ -0,0 +1,18 @@ +ARGUS Secure +Copyright (c) 2025-2026 AI4SEC / lmelo + +This product includes software developed by third parties under their own open-source +licenses. Full license texts and attributions are listed in THIRD_PARTY_LICENSES. + +Notable components: +- nginx (BSD-2-Clause) — Copyright (C) 2002-2024 Igor Sysoev, (C) 2011-2024 Nginx, Inc. +- ModSecurity (Apache-2.0) — Copyright (C) 2002-2024 Trustwave Holdings, Inc. +- OWASP Core Rule Set (Apache-2.0) — Copyright (C) 2006-2024 Trustwave Holdings, Inc., + OWASP Core Rule Set contributors +- TimescaleDB Community Edition (Timescale License) — see THIRD_PARTY_LICENSES for + redistribution terms +- MaxMind GeoLite2 (GeoLite2 EULA) — "This product includes GeoLite2 data created by + MaxMind, available from https://www.maxmind.com" +- Valkey (BSD-3-Clause) + +This repository's own source code is proprietary; see LICENSE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..03b2404 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +
+ +# ARGUS + +### Enterprise Web Security Platform + +[![License](https://img.shields.io/badge/License-Proprietary-lightgrey?style=for-the-badge)](LICENSE) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://github.com/alleyviper/argus/pkgs/container/argus-secure-api) + +[![Nginx](https://img.shields.io/badge/Nginx-1.30-009639?style=for-the-badge&logo=nginx&logoColor=white)](https://nginx.org/) +[![ModSecurity](https://img.shields.io/badge/ModSecurity-v3-red?style=for-the-badge)](https://modsecurity.org/) +[![OWASP CRS](https://img.shields.io/badge/OWASP_CRS-v4.26-orange?style=for-the-badge)](https://coreruleset.org/) +[![HTTP/3](https://img.shields.io/badge/HTTP/3-QUIC-blue?style=for-the-badge)]() + +

+ A real-time security operations platform for the modern web edge.
+ Reverse proxy, Web Application Firewall, bot defense, DNS protection, and live threat + investigation — in one appliance.
+

+ +

+ ✨ Features • + 🚀 Installation • + 🛠 Operations • + 📚 API +

+ +--- + +
+ +## ✨ Key Features + +### 🛡️ Web Application Firewall +ModSecurity v3 with the OWASP Core Rule Set. Paranoia levels 1–4, per-host rule exceptions, +detection-only or blocking modes. + +### 🌐 Attack Origin Visualization +A live global map of inbound attacks — real-time arcs from source to your infrastructure, +severity-coded, with instant drill-down into any event's full detail. + +### 🔎 Incident Investigation +Pivot from any IP, log line, or dashboard event straight into that entity's complete history — +affected hosts, triggered rules, geography, and live threat-intelligence reputation — without +losing your place. + +### 🤖 Bot & AI-Crawler Defense +Blocks known-bad bots and AI scrapers automatically, with a search-engine allowlist so legitimate +crawlers are never affected. Escalating challenge modes (browser check, proof-of-work, visual +CAPTCHA) for suspicious traffic. + +### 🧬 DNS Security Engine +An embedded forwarding DNS resolver with real-time threat detection — DGA domains, DNS +tunneling, fast-flux infrastructure, cache-poisoning attempts — and automatic mitigation via +sinkholing. + +### 🌍 GeoIP & Cloud-Provider Access Control +Allow or block traffic by country or hosting provider, with live visual feedback. + +### 🔒 Automated SSL +Let's Encrypt with automatic renewal, including wildcard certificates via DNS-01 challenge +(Cloudflare, DuckDNS, Dynu, and more). + +### ⚡ Rate Limiting & Auto-Ban +Configurable per-IP/per-URI rate limits, automatic banning on repeated WAF violations, and +optional community threat-intelligence-driven blocking. + +### 👥 Full User Management +Role-based access, per-user session control, forced password resets, account lock/unlock, MFA, +and a complete login history — everything a security team needs to administer access safely. + +### 📊 Real-Time Operations Dashboard +Live traffic, security events, and system health, updated continuously — no manual refresh. + +### 🔀 Load Balancing & Stream Proxying +Multiple backend servers with health checks, plus TCP/UDP stream proxying with optional SNI +routing. + +### 🔮 Modern Protocol Support +HTTP/3 (QUIC) and post-quantum-ready TLS (ML-KEM hybrid key exchange). + +--- + +## 🚀 Installation + +### Requirements + +- A Linux host with Docker 24.0+ and the Docker Compose plugin (`docker compose`) +- Ports 80/443 available for the reverse proxy, and 81 for the admin panel (all configurable) + +### One-line install + +```bash +curl -fsSL https://raw.githubusercontent.com/alleyviper/argus/main/install.sh | bash +``` + +This downloads the deployment files, generates a secure database password, creates the +required Docker network, pulls the production images, starts ARGUS, and prints your admin +login at the end. Installs to `~/argus` by default — set `ARGUS_INSTALL_DIR` first to change +that. + +### Manual install + +If you'd rather see every step: + +```bash +mkdir -p ~/argus/docker-socket-proxy && cd ~/argus +curl -fsSL https://raw.githubusercontent.com/alleyviper/argus/main/docker-compose.yml -o docker-compose.yml +curl -fsSL https://raw.githubusercontent.com/alleyviper/argus/main/.env.example -o .env +curl -fsSL https://raw.githubusercontent.com/alleyviper/argus/main/docker-socket-proxy/haproxy.cfg.template -o docker-socket-proxy/haproxy.cfg.template + +# Generate a secure database password +sed -i "s/DB_PASSWORD=.*/DB_PASSWORD=$(openssl rand -base64 24)/" .env + +# ARGUS expects this network to already exist +docker network create argus-network + +docker compose pull +docker compose up -d +``` + +Then log in at `https://localhost:81` with `admin` / `admin` and change the password +immediately — the one-line installer does this rotation for you automatically. + +**Do not expose the admin panel (port 81) to the internet.** Keep it on your LAN/VPN, or put +it behind its own proxy host with access lists and 2FA enabled. + +--- + +## 🛠 Operations + +Five scripts cover day-to-day operation — download them once alongside `docker-compose.yml` +(the one-line installer already does this for you), then run from `~/argus` (or wherever you +installed): + +| Script | Purpose | +|--------|---------| +| `./update.sh` | Pull the latest images and restart, with a before/after version check | +| `./healthcheck.sh` | Report the status of every component and the admin panel | +| `./backup.sh [dir]` | Back up the database and application data into one archive | +| `./restore.sh ` | Restore a backup onto a fresh installation | +| `./uninstall.sh [--remove-data]` | Stop ARGUS; data is kept unless you explicitly opt in to removing it | + +### Upgrading + +```bash +cd ~/argus +./update.sh +``` + +New versions are published to GHCR as soon as they're released — `update.sh` always pulls the +current stable release. This is the only supported upgrade path; there is no separate migration +step to run. + +### Backups + +```bash +cd ~/argus +./backup.sh # writes argus-backup-.tar.gz to the current directory +./restore.sh # onto a fresh installation only — see the script's own header +``` + +### Resetting the admin password + +```bash +docker compose exec api ./server reset-password +``` + +Add `--username ` to target a specific account, or `--password '...'` to set a known +value instead of generating one. See `docker compose exec api ./server reset-password --help` +for the rest of the options (clearing 2FA, etc.). + +### Threat intelligence integration (optional) + +ARGUS can connect to **ANIS**, a separately-deployed threat-intelligence hub, to share confirmed +attacker IPs and receive a community blocklist in return. It's off by default — set +`ANIS_ENABLED=true` and `ANIS_URL` in `.env` to turn it on. See the comments in `.env.example` +for the full set of options. + +--- + +## 📚 API + +ARGUS exposes a full REST API for automation. Authenticate with a JWT (`POST +/api/v1/auth/login`) for interactive use, or an API token (`Authorization: Bearer ng_`, +created from the admin panel) for CI/CD and scripted access. Interactive API documentation is +served at `https://localhost:81/api/docs` once ARGUS is running. + +--- + +## ⚙️ Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `DB_PASSWORD` | Database password | *(generated by the installer)* | +| `TZ` | Timezone | `UTC` | +| `UI_PORT` | Admin panel port | `81` | +| `NGINX_HTTP_PORT` / `NGINX_HTTPS_PORT` | Reverse-proxy listen ports | `80` / `443` | +| `API_HOST_PORT` | Internal API port exposed to the host | `9080` | +| `DNS_LISTEN_HOST` | DNS Security Engine bind address | `127.0.0.1` | +| `ANIS_ENABLED` / `ANIS_URL` | Optional threat-intel hub integration | `false` / *(empty)* | + +Full list with descriptions in `.env.example`. + +--- + +## 📄 License + +This is proprietary, commercial software — see [LICENSE](LICENSE). Third-party open-source +components retain their own licenses — see [THIRD_PARTY_LICENSES](THIRD_PARTY_LICENSES) and +[NOTICE](NOTICE). + +## 💬 Support + +[GitHub Issues](https://github.com/alleyviper/argus/issues) — bug reports and feature requests. + +--- + +
+ © 2025-2026 ARGUS — AI4SEC. +
diff --git a/THIRD_PARTY_LICENSES b/THIRD_PARTY_LICENSES new file mode 100644 index 0000000..586c83c --- /dev/null +++ b/THIRD_PARTY_LICENSES @@ -0,0 +1,112 @@ +# Third-Party Licenses + +ARGUS Secure uses the following third-party software: + +## Core Components + +### TimescaleDB +- **License**: Timescale License (TSL) +- **Website**: https://github.com/timescale/timescaledb +- **License URL**: https://github.com/timescale/timescaledb/blob/main/tsl/LICENSE-TIMESCALE +- **Note**: TimescaleDB Community Edition is used for time-series data storage and compression. + The Timescale License carries redistribution conditions beyond plain open source — reviewed + before bundling with a commercial, proprietary product; see legal review note below. + +### Nginx +- **License**: BSD-2-Clause +- **Website**: https://nginx.org/ +- **Copyright**: (C) 2002-2024 Igor Sysoev, (C) 2011-2024 Nginx, Inc. + +### ModSecurity +- **License**: Apache License 2.0 +- **Website**: https://github.com/owasp-modsecurity/ModSecurity +- **Copyright**: (C) 2002-2024 Trustwave Holdings, Inc. + +### OWASP Core Rule Set (CRS) +- **License**: Apache License 2.0 +- **Website**: https://coreruleset.org/ +- **Copyright**: (C) 2006-2024 Trustwave Holdings, Inc., OWASP Core Rule Set contributors + +### MaxMind GeoIP2 / GeoLite2 +- **License**: GeoLite2 End User License Agreement +- **Website**: https://www.maxmind.com/ +- **Attribution**: This product includes GeoLite2 data created by MaxMind, available from + https://www.maxmind.com. Redistribution terms are stricter than plain open source — reviewed + before bundling; see legal review note below. + +### Valkey +- **License**: BSD-3-Clause +- **Website**: https://valkey.io/ + +## Backend (Go) Dependencies + +| Package | License | Website | +|---------|---------|---------| +| github.com/labstack/echo/v4 | MIT | https://echo.labstack.com/ | +| github.com/go-acme/lego/v4 | MIT | https://go-acme.github.io/lego/ | +| github.com/lib/pq | MIT | https://github.com/lib/pq | +| github.com/google/uuid | BSD-3-Clause | https://github.com/google/uuid | +| github.com/redis/go-redis/v9 | BSD-2-Clause | https://github.com/redis/go-redis | +| github.com/oschwald/geoip2-golang | ISC | https://github.com/oschwald/geoip2-golang | +| github.com/cloudflare/cloudflare-go | BSD-3-Clause | https://github.com/cloudflare/cloudflare-go | +| golang.org/x/crypto | BSD-3-Clause | https://golang.org/x/crypto | + +## Frontend (React) Dependencies + +| Package | License | Website | +|---------|---------|---------| +| React | MIT | https://react.dev/ | +| @tanstack/react-query | MIT | https://tanstack.com/query | +| react-router-dom | MIT | https://reactrouter.com/ | +| recharts | MIT | https://recharts.org/ | +| i18next | MIT | https://www.i18next.com/ | +| Tailwind CSS | MIT | https://tailwindcss.com/ | +| Vite | MIT | https://vitejs.dev/ | + +## Nginx Modules + +| Module | License | Website | +|--------|---------|---------| +| ModSecurity-nginx | Apache 2.0 | https://github.com/owasp-modsecurity/ModSecurity-nginx | +| ngx_brotli | BSD-2-Clause | https://github.com/google/ngx_brotli | +| headers-more-nginx-module | BSD-2-Clause | https://github.com/openresty/headers-more-nginx-module | +| ngx_http_geoip2_module | BSD-2-Clause | https://github.com/leev/ngx_http_geoip2_module | + +--- + +## Apache License 2.0 Notice + +The following components are licensed under the Apache License 2.0: + +- ModSecurity (Copyright 2002-2024 Trustwave Holdings, Inc.) +- OWASP Core Rule Set (Copyright 2006-2024 Trustwave Holdings, Inc.) +- ModSecurity-nginx (Copyright 2015-2024 Trustwave Holdings, Inc.) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use these files except in compliance with the License. +You may obtain a copy of the License at: + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- + +## GeoLite2 End User License Agreement + +This product includes GeoLite2 data created by MaxMind, available from +https://www.maxmind.com. GeoLite2 databases are offered under the +GeoLite2 End User License Agreement. For the full license text, see: +https://www.maxmind.com/en/geolite2/eula + +--- + +## Pending legal review before commercial distribution + +TimescaleDB (Community Edition, TSL) and MaxMind GeoLite2 (EULA) both carry redistribution +conditions beyond plain MIT/BSD/Apache terms. Confirm these terms are compatible with the +proprietary commercial license in `LICENSE` before shipping a release build. diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..2c230dd --- /dev/null +++ b/backup.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# +# ARGUS backup — dumps the database and application data (certificates, +# uploaded config) into a single, timestamped archive. +# +# cd ~/argus && ./backup.sh [output-directory] +# +set -euo pipefail + +INSTALL_DIR="${ARGUS_INSTALL_DIR:-$HOME/argus}" +OUT_DIR="${1:-$PWD}" +STAMP="$(date +%Y%m%d-%H%M%S)" +ARCHIVE="${OUT_DIR}/argus-backup-${STAMP}.tar.gz" + +bold() { printf '\033[1m%s\033[0m\n' "$1"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +fail() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; } + +[ -f "$INSTALL_DIR/docker-compose.yml" ] || fail "No ARGUS installation found at $INSTALL_DIR (set ARGUS_INSTALL_DIR if you installed elsewhere)." +cd "$INSTALL_DIR" + +bold "Backing up ARGUS..." + +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +docker compose exec -T db pg_dump -U "${DB_USER:-postgres}" "${DB_NAME:-argus}" | gzip > "$WORKDIR/db.sql.gz" +ok "Database dumped" + +docker run --rm -v argus_api_data:/data -v "$WORKDIR":/backup alpine \ + tar czf /backup/api-data.tar.gz -C /data . >/dev/null 2>&1 +ok "Application data archived" + +mkdir -p "$OUT_DIR" +tar czf "$ARCHIVE" -C "$WORKDIR" db.sql.gz api-data.tar.gz +ok "Backup written to ${ARCHIVE}" + +echo +printf ' %s\n' "Restore with: ./restore.sh ${ARCHIVE}" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a70abd6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,229 @@ +services: + # TimescaleDB (PostgreSQL with time-series optimization) + db: + image: timescale/timescaledb:latest-pg17 + container_name: argus-db + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "3" + environment: + TZ: ${TZ:-UTC} + POSTGRES_USER: ${DB_USER:-postgres} + POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} + POSTGRES_DB: ${DB_NAME:-argus} + command: postgres -c timezone=${TZ:-UTC} -c log_timezone=${TZ:-UTC} -c shared_preload_libraries=timescaledb -c timescaledb.telemetry_level=off -c max_locks_per_transaction=512 + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + # Valkey (Redis-compatible) for caching + valkey: + image: valkey/valkey:9-alpine + container_name: argus-valkey + restart: unless-stopped + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + environment: + TZ: ${TZ:-UTC} + command: > + valkey-server + --maxmemory 256mb + --maxmemory-policy allkeys-lru + --appendonly yes + --appendfsync everysec + volumes: + - valkey_data:/data + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + # Restricts the api service's Docker Engine API access to exactly what it + # needs (list/inspect/exec/restart existing containers for log collection + # and nginx reload) — it cannot create new containers or touch the host + # beyond that. See docker-socket-proxy/README.md for the exact allowed set. + docker-socket-proxy: + image: tecnativa/docker-socket-proxy:latest + container_name: argus-docker-socket-proxy + restart: unless-stopped + security_opt: + - "no-new-privileges:true" + environment: + CONTAINERS: 1 + IMAGES: 1 + VOLUMES: 1 + SYSTEM: 1 + EXEC: 1 + VERSION: 1 + POST: 1 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./docker-socket-proxy/haproxy.cfg.template:/usr/local/etc/haproxy/haproxy.cfg.template:ro + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Go API Server + api: + image: ghcr.io/alleyviper/argus-secure-api:latest + container_name: argus-api + restart: unless-stopped + # Container hardening: no-new-privileges blocks setuid privilege + # escalation; cap_drop ALL removes every Linux capability, re-adding only + # what's needed (DAC_OVERRIDE for the shared /etc/nginx volume owned by + # the nginx user, NET_BIND_SERVICE for the DNS Security Engine's :53). + security_opt: + - "no-new-privileges:true" + cap_drop: + - ALL + cap_add: + - DAC_OVERRIDE + - NET_BIND_SERVICE + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + TZ: ${TZ:-UTC} + PORT: "8080" + DATABASE_URL: postgres://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@db:5432/${DB_NAME:-argus}?sslmode=disable + REDIS_URL: redis://valkey:6379/0 + ENVIRONMENT: ${ENVIRONMENT:-production} + NGINX_CONTAINER: argus-proxy + NGINX_SKIP_TEST: "false" + NGINX_STATUS_URL: "http://host.docker.internal:${NGINX_HTTP_PORT:-80}/nginx_status" + NGINX_ACCESS_LOG: "/etc/nginx/logs/access_raw.log" + BACKUP_PATH: ${BACKUP_PATH:-/app/data/backups} + DOCKER_API_VERSION: ${DOCKER_API_VERSION:-} + DOCKER_HOST: tcp://docker-socket-proxy:2375 + NGINX_HTTP_PORT: ${NGINX_HTTP_PORT:-} + NGINX_HTTPS_PORT: ${NGINX_HTTPS_PORT:-} + API_HOST_PORT: ${API_HOST_PORT:-9080} + API_HOST: ${API_HOST:-} + # ANIS community intelligence — optional, off by default. Set these to + # connect this instance to an already-running ANIS hub. + ANIS_ENABLED: ${ANIS_ENABLED:-false} + ANIS_URL: ${ANIS_URL:-} + ANIS_LICENSE_KEY: ${ANIS_LICENSE_KEY:-} + ANIS_ADMIN_KEY: ${ANIS_ADMIN_KEY:-} + ANIS_SHARE_ATTACKERS: ${ANIS_SHARE_ATTACKERS:-false} + DNS_SECURITY_LISTEN_ADDR: ":53" + ports: + - "127.0.0.1:${API_HOST_PORT:-9080}:8080" + # DNS Security Engine. Host-only by default — a bare forwarding + # resolver reachable from the network is exactly the profile abused + # for DNS amplification attacks against third parties. Set + # DNS_LISTEN_HOST to an internal interface IP to route real client + # DNS traffic through it (never 0.0.0.0 on an untrusted network). + - "${DNS_LISTEN_HOST:-127.0.0.1}:53:53/udp" + - "${DNS_LISTEN_HOST:-127.0.0.1}:53:53/tcp" + volumes: + - nginx_data:/etc/nginx:rw + - api_data:/app/data:rw + depends_on: + db: + condition: service_healthy + valkey: + condition: service_started + docker-socket-proxy: + condition: service_started + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 360s + + # React UI (Admin Panel), served over HTTPS + ui: + image: ghcr.io/alleyviper/argus-secure-ui:latest + container_name: argus-ui + restart: unless-stopped + security_opt: + - "no-new-privileges:true" + cap_drop: + - ALL + cap_add: + - NET_BIND_SERVICE + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + environment: + TZ: ${TZ:-UTC} + ports: + - "${UI_PORT:-81}:443" + volumes: + - ui_data:/app/ssl:rw + depends_on: + - api + + # Nginx reverse proxy — host network mode, so real client IPs are visible + # directly without needing PROXY protocol. + nginx: + image: ghcr.io/alleyviper/argus-secure-nginx:latest + container_name: argus-proxy + restart: always + network_mode: host + # Not cap_drop'd: runs nginx in host-network mode with a root master that + # drops workers to the nginx user, needing CHOWN/SETUID/SETGID plus binds + # on privileged 80/443. + security_opt: + - "no-new-privileges:true" + logging: + driver: "json-file" + options: + max-size: "100m" + max-file: "5" + environment: + TZ: ${TZ:-UTC} + ulimits: + nofile: + soft: 65535 + hard: 65535 + volumes: + - nginx_data:/etc/nginx:rw + depends_on: + - api + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:${NGINX_HTTP_PORT:-80}/health"] + interval: 15s + timeout: 5s + retries: 3 + start_period: 10s + +volumes: + postgres_data: + name: argus_postgres_data + valkey_data: + name: argus_valkey_data + nginx_data: + name: argus_nginx_data + api_data: + name: argus_api_data + ui_data: + name: argus_ui_data + +networks: + default: + name: argus-network + external: true diff --git a/docker-socket-proxy/haproxy.cfg.template b/docker-socket-proxy/haproxy.cfg.template new file mode 100644 index 0000000..9396f23 --- /dev/null +++ b/docker-socket-proxy/haproxy.cfg.template @@ -0,0 +1,97 @@ +global + log stdout format raw daemon "${LOG_LEVEL}" + + pidfile /run/haproxy.pid + maxconn 4000 + + # Turn on stats unix socket + server-state-file /var/lib/haproxy/server-state + +defaults + mode http + log global + option httplog + option dontlognull + option http-server-close + option redispatch + retries 3 + timeout http-request 10s + timeout queue 1m + timeout connect 10s + timeout client 10m + timeout server 10m + timeout http-keep-alive 10s + timeout check 10s + maxconn 3000 + + # Allow seamless reloads + load-server-state-from-file global + + # Use provided example error pages + errorfile 400 /usr/local/etc/haproxy/errors/400.http + errorfile 403 /usr/local/etc/haproxy/errors/403.http + errorfile 408 /usr/local/etc/haproxy/errors/408.http + errorfile 500 /usr/local/etc/haproxy/errors/500.http + errorfile 502 /usr/local/etc/haproxy/errors/502.http + errorfile 503 /usr/local/etc/haproxy/errors/503.http + errorfile 504 /usr/local/etc/haproxy/errors/504.http + +backend dockerbackend + server dockersocket $SOCKET_PATH + +backend docker-events + server dockersocket $SOCKET_PATH + timeout server 0 + +frontend dockerfrontend + bind ${BIND_CONFIG} + http-request deny unless METH_GET || { env(POST) -m bool } + + # --- ARGUS-specific hardening, layered on top of the stock tecnativa + # image (see docker/docker-socket-proxy/README.md) --- + # + # The stock template's per-category CONTAINERS=1 gate is a path-prefix + # match on the whole /containers tree — once POST is globally unlocked + # (required here for restart/exec/pull/tag), CONTAINERS=1 alone would + # also let POST /containers/create through, which defeats the entire + # point of this proxy (an attacker who compromises the API could create + # a brand-new container with a host bind mount and escape to the host, + # the same class of privesc that raw docker.sock access has always + # allowed). Explicitly deny the dangerous, unused-by-ARGUS container + # lifecycle endpoints *before* the generic CONTAINERS allow rule below, + # regardless of which category env vars are set: + http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/create(\?.*)?$ } + http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/prune(\?.*)?$ } + http-request deny if METH_DELETE { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+$ } + + # Allowed endpoints + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/((stop)|(restart)|(kill)) } { env(ALLOW_RESTARTS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/start } { env(ALLOW_START) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/[a-zA-Z0-9_.-]+/stop } { env(ALLOW_STOP) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/auth } { env(AUTH) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/build } { env(BUILD) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/commit } { env(COMMIT) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/configs } { env(CONFIGS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers } { env(CONTAINERS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/distribution } { env(DISTRIBUTION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } { env(EVENTS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec } { env(EXEC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/grpc } { env(GRPC) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images } { env(IMAGES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/info } { env(INFO) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/networks } { env(NETWORKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/nodes } { env(NODES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping } { env(PING) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/plugins } { env(PLUGINS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/secrets } { env(SECRETS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/services } { env(SERVICES) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/session } { env(SESSION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/swarm } { env(SWARM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/system } { env(SYSTEM) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/tasks } { env(TASKS) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/version } { env(VERSION) -m bool } + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes } { env(VOLUMES) -m bool } + http-request deny + default_backend dockerbackend + + use_backend docker-events if { path,url_dec -m reg -i ^(/v[\d\.]+)?/events } diff --git a/healthcheck.sh b/healthcheck.sh new file mode 100755 index 0000000..086efa0 --- /dev/null +++ b/healthcheck.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# +# ARGUS health check — reports the status of every component. +# +# cd ~/argus && ./healthcheck.sh +# +set -euo pipefail + +INSTALL_DIR="${ARGUS_INSTALL_DIR:-$HOME/argus}" + +bold() { printf '\033[1m%s\033[0m\n' "$1"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +bad() { printf ' \033[31m✗\033[0m %s\n' "$1"; } +fail() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; } + +[ -f "$INSTALL_DIR/docker-compose.yml" ] || fail "No ARGUS installation found at $INSTALL_DIR (set ARGUS_INSTALL_DIR if you installed elsewhere)." +cd "$INSTALL_DIR" + +bold "ARGUS Health Check" +echo + +EXIT_CODE=0 + +bold "Containers" +for svc in db valkey docker-socket-proxy api ui nginx; do + STATE="$(docker compose ps --format json "$svc" 2>/dev/null | grep -oP '"State"\s*:\s*"\K[^"]+' || echo "")" + HEALTH="$(docker compose ps --format json "$svc" 2>/dev/null | grep -oP '"Health"\s*:\s*"\K[^"]+' || echo "")" + if [ "$STATE" = "running" ] && { [ -z "$HEALTH" ] || [ "$HEALTH" = "healthy" ]; }; then + ok "$svc — running${HEALTH:+ ($HEALTH)}" + elif [ -z "$STATE" ]; then + bad "$svc — not found" + EXIT_CODE=1 + else + bad "$svc — $STATE${HEALTH:+ ($HEALTH)}" + EXIT_CODE=1 + fi +done +echo + +bold "API" +API_HOST_PORT="$(grep -oP '^API_HOST_PORT=\K.*' .env 2>/dev/null || true)" +API_HOST_PORT="${API_HOST_PORT:-9080}" +HEALTH_JSON="$(curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" 2>/dev/null || true)" +if [ -n "$HEALTH_JSON" ]; then + VERSION="$(printf '%s' "$HEALTH_JSON" | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "?")" + DB_STATUS="$(printf '%s' "$HEALTH_JSON" | grep -oP '"database"\s*:\s*"\K[^"]+' || echo "?")" + CACHE_STATUS="$(printf '%s' "$HEALTH_JSON" | grep -oP '"cache"\s*:\s*"\K[^"]+' || echo "?")" + ok "API reachable — version ${VERSION}, database: ${DB_STATUS}, cache: ${CACHE_STATUS}" +else + bad "API not reachable on http://127.0.0.1:${API_HOST_PORT}/health" + EXIT_CODE=1 +fi +echo + +UI_PORT="$(grep -oP '^UI_PORT=\K.*' .env 2>/dev/null || true)" +UI_PORT="${UI_PORT:-81}" +bold "Admin panel" +if curl -fsSk -o /dev/null "https://127.0.0.1:${UI_PORT}" 2>/dev/null; then + ok "Reachable at https://localhost:${UI_PORT}" +else + bad "Not reachable at https://localhost:${UI_PORT}" + EXIT_CODE=1 +fi + +echo +if [ "$EXIT_CODE" = 0 ]; then + bold "All checks passed." +else + bold "One or more checks failed — see above. 'docker compose logs ' for details." +fi +exit "$EXIT_CODE" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..5235a8e --- /dev/null +++ b/install.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# +# ARGUS installer — fetches the production compose stack, generates secrets, +# starts the platform, and prints your admin credentials. +# +# curl -fsSL https://raw.githubusercontent.com/alleyviper/argus/main/install.sh | bash +# +set -euo pipefail + +REPO_RAW_BASE="https://raw.githubusercontent.com/alleyviper/argus/main" +INSTALL_DIR="${ARGUS_INSTALL_DIR:-$HOME/argus}" +NETWORK_NAME="argus-network" + +# ── Output helpers ──────────────────────────────────────────────────────────── +bold() { printf '\033[1m%s\033[0m\n' "$1"; } +info() { printf ' %s\n' "$1"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +warn() { printf ' \033[33m!\033[0m %s\n' "$1"; } +fail() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; } + +bold "ARGUS Installer" +echo + +# ── 1. OS detection (informational — Linux is the supported target) ────────── +OS="$(uname -s)" +case "$OS" in + Linux) ok "Detected Linux" ;; + Darwin) warn "Detected macOS — supported for local evaluation via Docker Desktop, not for production deployment." ;; + *) warn "Unrecognized OS ($OS) — proceeding, but only Linux is officially supported." ;; +esac + +# ── 2. Docker / Compose availability ────────────────────────────────────────── +command -v docker >/dev/null 2>&1 || fail "Docker is not installed. Install Docker first: https://docs.docker.com/engine/install/" +docker info >/dev/null 2>&1 || fail "Docker is installed but not running (or this user lacks permission). Start Docker / add your user to the docker group, then re-run." +docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 (the 'docker compose' plugin) is required. See https://docs.docker.com/compose/install/" +ok "Docker and Docker Compose are available" + +# ── 3. Fetch deployment files ────────────────────────────────────────────────── +mkdir -p "$INSTALL_DIR/docker-socket-proxy" +cd "$INSTALL_DIR" + +fetch() { + curl -fsSL "$REPO_RAW_BASE/$1" -o "$2" || fail "Failed to download $1" +} + +fetch "docker-compose.yml" "docker-compose.yml" +fetch "docker-socket-proxy/haproxy.cfg.template" "docker-socket-proxy/haproxy.cfg.template" +for script in update.sh uninstall.sh healthcheck.sh backup.sh restore.sh; do + fetch "$script" "$script" + chmod +x "$script" +done +if [ ! -f .env ]; then + fetch ".env.example" ".env" + ok "Fetched deployment files into $INSTALL_DIR" +else + ok "Fetched compose file (kept your existing .env)" +fi + +# ── 4. Generate secrets (first install only — never overwrite an existing .env) ── +if grep -q '^DB_PASSWORD=change-me-in-production' .env 2>/dev/null; then + DB_PASSWORD="$(openssl rand -base64 24 | tr -d '\n')" + sed -i.bak "s#^DB_PASSWORD=.*#DB_PASSWORD=${DB_PASSWORD}#" .env && rm -f .env.bak + ok "Generated a secure database password" +fi + +# ── 5. Docker network ───────────────────────────────────────────────────────── +if docker network inspect "$NETWORK_NAME" >/dev/null 2>&1; then + ok "Docker network '$NETWORK_NAME' already exists" +else + docker network create "$NETWORK_NAME" >/dev/null + ok "Created Docker network '$NETWORK_NAME'" +fi + +# ── 6. Pull and start ────────────────────────────────────────────────────────── +bold "Pulling images and starting ARGUS..." +docker compose pull +docker compose up -d +echo + +# ── 7. Wait for the API to become healthy ───────────────────────────────────── +API_HOST_PORT="$(grep -oP '^API_HOST_PORT=\K.*' .env 2>/dev/null || true)" +API_HOST_PORT="${API_HOST_PORT:-9080}" +bold "Waiting for ARGUS to finish starting (this can take a few minutes on first boot)..." +READY=false +for _ in $(seq 1 90); do + if curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" >/dev/null 2>&1; then + READY=true + break + fi + sleep 5 +done +if [ "$READY" != "true" ]; then + warn "ARGUS did not report healthy within 7.5 minutes." + warn "Check container status with: docker compose -f $INSTALL_DIR/docker-compose.yml ps" + warn "and logs with: docker compose -f $INSTALL_DIR/docker-compose.yml logs api" + exit 1 +fi +ok "ARGUS is up" + +# ── 8. Rotate the default admin credentials ─────────────────────────────────── +ADMIN_USER="admin" +ADMIN_PASSWORD="Ax9!$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 20)" +API_BASE="http://127.0.0.1:${API_HOST_PORT}/api/v1" + +LOGIN_RESP="$(curl -fsS -X POST "${API_BASE}/auth/login" \ + -H 'Content-Type: application/json' \ + -d '{"username":"admin","password":"admin"}' 2>/dev/null || true)" +TOKEN="$(printf '%s' "${LOGIN_RESP}" | grep -oP '"token"\s*:\s*"\K[^"]+' || true)" + +CREDS_ROTATED=false +if [ -n "$TOKEN" ]; then + CHANGE_RESP="$(curl -fsS -o /dev/null -w '%{http_code}' -X POST "${API_BASE}/auth/change-credentials" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer ${TOKEN}" \ + -d "{\"current_password\":\"admin\",\"new_username\":\"${ADMIN_USER}\",\"new_password\":\"${ADMIN_PASSWORD}\",\"new_password_confirm\":\"${ADMIN_PASSWORD}\"}" 2>/dev/null || echo "000")" + if [ "$CHANGE_RESP" = "200" ]; then + CREDS_ROTATED=true + ok "Rotated the default admin password" + fi +fi +if [ "$CREDS_ROTATED" != "true" ]; then + warn "Could not auto-rotate admin credentials (already changed on a prior run?). Log in with your existing credentials, or admin/admin on a genuinely fresh install." +fi + +# ── 9. Summary ───────────────────────────────────────────────────────────────── +UI_PORT="$(grep -oP '^UI_PORT=\K.*' .env 2>/dev/null || true)" +UI_PORT="${UI_PORT:-81}" + +echo +bold "ARGUS is installed and running." +echo +info "Admin Panel: https://localhost:${UI_PORT} (accept the self-signed certificate)" +if [ "$CREDS_ROTATED" = "true" ]; then + info "Username: ${ADMIN_USER}" + info "Password: ${ADMIN_PASSWORD}" + warn "Save this password now — it is not stored anywhere and cannot be recovered." +else + info "Log in with your existing admin credentials." +fi +echo +info "Configuration: ${INSTALL_DIR}/.env and ${INSTALL_DIR}/docker-compose.yml" +info "Upgrade: cd ${INSTALL_DIR} && docker compose pull && docker compose up -d" +info "Backups: see https://github.com/alleyviper/argus#backups" +echo diff --git a/restore.sh b/restore.sh new file mode 100755 index 0000000..7a97a5f --- /dev/null +++ b/restore.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# +# ARGUS restore — loads a backup created by backup.sh into a FRESH ARGUS +# installation (empty database, no prior data). To restore onto a machine +# that already has data on it, uninstall.sh --remove-data && install.sh +# first, then run this. +# +# cd ~/argus && ./restore.sh argus-backup-20260719-120000.tar.gz +# +set -euo pipefail + +INSTALL_DIR="${ARGUS_INSTALL_DIR:-$HOME/argus}" +ARCHIVE="${1:-}" + +bold() { printf '\033[1m%s\033[0m\n' "$1"; } +info() { printf ' %s\n' "$1"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +warn() { printf ' \033[33m!\033[0m %s\n' "$1"; } +fail() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; } + +[ -n "$ARCHIVE" ] || fail "Usage: ./restore.sh " +[ -f "$ARCHIVE" ] || fail "No such file: $ARCHIVE" +[ -f "$INSTALL_DIR/docker-compose.yml" ] || fail "No ARGUS installation found at $INSTALL_DIR (set ARGUS_INSTALL_DIR if you installed elsewhere)." +cd "$INSTALL_DIR" + +warn "This assumes a freshly installed, empty ARGUS instance." +warn "Restoring onto an instance that already has data will fail or corrupt it." +printf " Type YES to continue: " +read -r CONFIRM +[ "$CONFIRM" = "YES" ] || fail "Aborted." + +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT +tar xzf "$ARCHIVE" -C "$WORKDIR" +[ -f "$WORKDIR/db.sql.gz" ] && [ -f "$WORKDIR/api-data.tar.gz" ] || fail "Not a valid ARGUS backup archive (missing db.sql.gz / api-data.tar.gz)." + +bold "Stopping the API (keeping the database up)..." +docker compose stop api ui nginx >/dev/null + +bold "Restoring database..." +gunzip -c "$WORKDIR/db.sql.gz" | docker compose exec -T db psql -U "${DB_USER:-postgres}" "${DB_NAME:-argus}" >/dev/null +ok "Database restored" + +bold "Restoring application data..." +docker run --rm -v argus_api_data:/data -v "$WORKDIR":/backup alpine \ + sh -c "rm -rf /data/* && tar xzf /backup/api-data.tar.gz -C /data" >/dev/null 2>&1 +ok "Application data restored" + +bold "Starting ARGUS..." +docker compose up -d +echo +ok "Restore complete" +info "Give it a minute to finish starting, then check: ./healthcheck.sh" diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..689faba --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# ARGUS uninstaller — stops and removes ARGUS containers. +# +# By default this KEEPS your data (database, certificates, uploaded config) — +# it only stops and removes containers, so a plain re-install (install.sh) +# picks up right where you left off. +# +# cd ~/argus && ./uninstall.sh # containers only, data kept +# ./uninstall.sh --remove-data # also destroys all data (asks to confirm) +# ./uninstall.sh --remove-data --yes # non-interactive, for scripted use +# +set -euo pipefail + +INSTALL_DIR="${ARGUS_INSTALL_DIR:-$HOME/argus}" +REMOVE_DATA=false +ASSUME_YES=false + +for arg in "$@"; do + case "$arg" in + --remove-data) REMOVE_DATA=true ;; + --yes|-y) ASSUME_YES=true ;; + *) echo "Unknown option: $arg" >&2; exit 1 ;; + esac +done + +bold() { printf '\033[1m%s\033[0m\n' "$1"; } +info() { printf ' %s\n' "$1"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +warn() { printf ' \033[33m!\033[0m %s\n' "$1"; } +fail() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; } + +bold "ARGUS Uninstaller" +echo + +[ -f "$INSTALL_DIR/docker-compose.yml" ] || fail "No ARGUS installation found at $INSTALL_DIR (set ARGUS_INSTALL_DIR if you installed elsewhere)." +cd "$INSTALL_DIR" + +if [ "$REMOVE_DATA" = "true" ] && [ "$ASSUME_YES" != "true" ]; then + warn "This will permanently delete your ARGUS database, certificates, and all" + warn "uploaded configuration. This cannot be undone." + printf " Type YES to confirm: " + read -r CONFIRM + [ "$CONFIRM" = "YES" ] || fail "Aborted — data was not touched." +fi + +if [ "$REMOVE_DATA" = "true" ]; then + bold "Stopping ARGUS and removing all data..." + docker compose down -v + ok "Containers and data volumes removed" +else + bold "Stopping ARGUS (data preserved)..." + docker compose down + ok "Containers removed — database and certificates are untouched" + info "Re-run install.sh (or 'docker compose up -d' here) to bring ARGUS back with the same data." +fi + +echo +info "Configuration files remain at ${INSTALL_DIR} — remove that directory yourself if you want a completely clean slate." diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..4d13035 --- /dev/null +++ b/update.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# +# ARGUS updater — pulls the latest published images and restarts the stack. +# +# cd ~/argus && ./update.sh +# (or, if you don't have it locally yet) +# curl -fsSL https://raw.githubusercontent.com/alleyviper/argus/main/update.sh | bash +# +set -euo pipefail + +INSTALL_DIR="${ARGUS_INSTALL_DIR:-$HOME/argus}" + +bold() { printf '\033[1m%s\033[0m\n' "$1"; } +info() { printf ' %s\n' "$1"; } +ok() { printf ' \033[32m✓\033[0m %s\n' "$1"; } +fail() { printf ' \033[31m✗\033[0m %s\n' "$1" >&2; exit 1; } + +bold "ARGUS Updater" +echo + +[ -f "$INSTALL_DIR/docker-compose.yml" ] || fail "No ARGUS installation found at $INSTALL_DIR (set ARGUS_INSTALL_DIR if you installed elsewhere)." +cd "$INSTALL_DIR" + +API_HOST_PORT="$(grep -oP '^API_HOST_PORT=\K.*' .env 2>/dev/null || true)" +API_HOST_PORT="${API_HOST_PORT:-9080}" + +BEFORE_VERSION="$(curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")" +info "Current version: ${BEFORE_VERSION}" + +bold "Pulling latest images..." +docker compose pull + +bold "Restarting..." +docker compose up -d +echo + +bold "Waiting for ARGUS to become healthy..." +READY=false +for _ in $(seq 1 60); do + if curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" >/dev/null 2>&1; then + READY=true + break + fi + sleep 5 +done + +if [ "$READY" != "true" ]; then + fail "ARGUS did not come back healthy within 5 minutes. Check: docker compose -f $INSTALL_DIR/docker-compose.yml logs" +fi + +AFTER_VERSION="$(curl -fsS "http://127.0.0.1:${API_HOST_PORT}/health" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")" +ok "ARGUS is healthy — now running version ${AFTER_VERSION}" +if [ "$BEFORE_VERSION" != "$AFTER_VERSION" ]; then + info "Updated: ${BEFORE_VERSION} → ${AFTER_VERSION}" +else + info "Already up to date." +fi