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://github.com/alleyviper/argus/pkgs/container/argus-secure-api)
+
+[](https://nginx.org/)
+[](https://modsecurity.org/)
+[](https://coreruleset.org/)
+[]()
+
+
+ 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