From 46122d5a7f4c68d7b87feba254c99cc30657e7c5 Mon Sep 17 00:00:00 2001 From: Gui-Gos <97973228+Gui-Gos@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:26:29 +0100 Subject: [PATCH] Initial commit - Runtipi Appstore with Wazuh 4.14.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added Wazuh 4.14.1 SIEM/XDR application for Runtipi - Simplified init scripts following official Wazuh Docker patterns - Complete documentation in French (description.md) - Health check diagnostic script (wazuh-health-check.sh) - SSL/TLS certificates auto-generation - Whoami test application included 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .DS_Store | Bin 0 -> 6148 bytes .github/workflows/renovate.yml | 45 ++ .github/workflows/test.yml | 23 + .gitignore | 1 + LICENSE | 13 + README.md | 31 + __tests__/apps.test.ts | 77 ++ apps/.DS_Store | Bin 0 -> 6148 bytes apps/wazuh-runtipi/.DS_Store | Bin 0 -> 6148 bytes apps/wazuh-runtipi/config.json | 57 ++ apps/wazuh-runtipi/data/.DS_Store | Bin 0 -> 6148 bytes apps/wazuh-runtipi/data/config/certs.yml | 12 + .../data/debug/wazuh-health-check.sh | 569 +++++++++++++ apps/wazuh-runtipi/data/scripts/init-certs.sh | 72 ++ .../data/scripts/init-dashboard.sh | 134 +++ .../data/scripts/init-indexer-init.sh | 56 ++ .../data/scripts/init-manager.sh | 100 +++ apps/wazuh-runtipi/docker-compose.json | 406 ++++++++++ apps/wazuh-runtipi/metadata/description.md | 762 ++++++++++++++++++ apps/wazuh-runtipi/metadata/logo.jpg | Bin 0 -> 53169 bytes apps/whoami/config.json | 24 + apps/whoami/docker-compose.json | 10 + apps/whoami/metadata/description.md | 43 + apps/whoami/metadata/logo.jpg | Bin 0 -> 18692 bytes bun.lockb | Bin 0 -> 9101 bytes config.js | 3 + package.json | 25 + renovate.json | 62 ++ scripts/update-config.ts | 35 + tsconfig.json | 24 + 30 files changed, 2584 insertions(+) create mode 100644 .DS_Store create mode 100644 .github/workflows/renovate.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 __tests__/apps.test.ts create mode 100644 apps/.DS_Store create mode 100644 apps/wazuh-runtipi/.DS_Store create mode 100644 apps/wazuh-runtipi/config.json create mode 100644 apps/wazuh-runtipi/data/.DS_Store create mode 100644 apps/wazuh-runtipi/data/config/certs.yml create mode 100644 apps/wazuh-runtipi/data/debug/wazuh-health-check.sh create mode 100644 apps/wazuh-runtipi/data/scripts/init-certs.sh create mode 100644 apps/wazuh-runtipi/data/scripts/init-dashboard.sh create mode 100644 apps/wazuh-runtipi/data/scripts/init-indexer-init.sh create mode 100644 apps/wazuh-runtipi/data/scripts/init-manager.sh create mode 100644 apps/wazuh-runtipi/docker-compose.json create mode 100644 apps/wazuh-runtipi/metadata/description.md create mode 100644 apps/wazuh-runtipi/metadata/logo.jpg create mode 100644 apps/whoami/config.json create mode 100644 apps/whoami/docker-compose.json create mode 100644 apps/whoami/metadata/description.md create mode 100644 apps/whoami/metadata/logo.jpg create mode 100755 bun.lockb create mode 100644 config.js create mode 100644 package.json create mode 100644 renovate.json create mode 100644 scripts/update-config.ts create mode 100644 tsconfig.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..71d98e9527ff91d70f53e361bfd1b6cdaad7d5b8 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4I%_5-~#+~L;|T|&(V2yybzdCg`Sc9Vy71EH#D_~ z= + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..41d64e0 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Example App Store Template + +This repository serves as a template for creating your own custom app store for the Runtipi platform. Use this as a starting point to create and share your own collection of applications. + +## Repository Structure + +- **apps/**: Contains individual app directories + + - Each app has its own folder (e.g., `whoami/`) with the following structure: + - `config.json`: App configuration file + - `docker-compose.json`: Docker setup for the app + - `metadata/`: Contains app visuals and descriptions + - `description.md`: Markdown description of the app + - `logo.jpg`: App logo image + +- **tests/**: Contains test files for the app store + + - `apps.test.ts`: Test suite for validating apps + +## Getting Started + +This repository is intended to serve as a template for creating your own app store. Follow these steps to get started: + +1. Click the "Use this template" button to create a new repository based on this template +2. Customize the apps or add your own app folders in the `apps/` directory +3. Test your app store by using it with Runtipi + +## Documentation + +For detailed instructions on creating your own app store, please refer to the official guide: +[Create Your Own App Store Guide](https://runtipi.io/docs/guides/create-your-own-app-store) diff --git a/__tests__/apps.test.ts b/__tests__/apps.test.ts new file mode 100644 index 0000000..4fd30c9 --- /dev/null +++ b/__tests__/apps.test.ts @@ -0,0 +1,77 @@ +import { expect, test, describe } from "bun:test"; +import { appInfoSchema, dynamicComposeSchema } from '@runtipi/common/schemas' +import { fromError } from 'zod-validation-error'; +import fs from 'node:fs' +import path from 'node:path' + +const getApps = async () => { + const appsDir = await fs.promises.readdir(path.join(process.cwd(), 'apps')) + + const appDirs = appsDir.filter((app) => { + const stat = fs.statSync(path.join(process.cwd(), 'apps', app)) + return stat.isDirectory() + }) + + return appDirs +}; + +const getFile = async (app: string, file: string) => { + const filePath = path.join(process.cwd(), 'apps', app, file) + try { + const file = await fs.promises.readFile(filePath, 'utf-8') + return file + } catch (err) { + return null + } +} + +describe("each app should have the required files", async () => { + const apps = await getApps() + + for (const app of apps) { + const files = ['config.json', 'docker-compose.json', 'metadata/logo.jpg', 'metadata/description.md'] + + for (const file of files) { + test(`app ${app} should have ${file}`, async () => { + const fileContent = await getFile(app, file) + expect(fileContent).not.toBeNull() + }) + } + } +}) + +describe("each app should have a valid config.json", async () => { + const apps = await getApps() + + for (const app of apps) { + test(`app ${app} should have a valid config.json`, async () => { + const fileContent = await getFile(app, 'config.json') + const parsed = appInfoSchema.omit({ urn: true }).safeParse(JSON.parse(fileContent || '{}')) + + if (!parsed.success) { + const validationError = fromError(parsed.error); + console.error(`Error parsing config.json for app ${app}:`, validationError.toString()); + } + + expect(parsed.success).toBe(true) + }) + } +}) + +describe("each app should have a valid docker-compose.json", async () => { + const apps = await getApps() + + for (const app of apps) { + test(`app ${app} should have a valid docker-compose.json`, async () => { + const fileContent = await getFile(app, 'docker-compose.json') + const parsed = dynamicComposeSchema.safeParse(JSON.parse(fileContent || '{}')) + + if (!parsed.success) { + const validationError = fromError(parsed.error); + console.error(`Error parsing docker-compose.json for app ${app}:`, validationError.toString()); + } + + expect(parsed.success).toBe(true) + }) + } +}); diff --git a/apps/.DS_Store b/apps/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a1a3d3549d6a8507ca9b78082d0da2e42612d469 GIT binary patch literal 6148 zcmeHKJ5EC}5S)b+kx-95=>HE%T1f#Za8e4`WO=(>@JUr$N00Mb+vs<6&-tReaUK*7 mQI3gGj=Au1d>KiZ*L=?XUN|HMo$;U(^)ukQ$fUr3EAR~|C>FB- literal 0 HcmV?d00001 diff --git a/apps/wazuh-runtipi/.DS_Store b/apps/wazuh-runtipi/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7b9d33741ed04ce8ac5299ff580d34195ae35713 GIT binary patch literal 6148 zcmeH~F^XlKsY>SV?}7 zi2>N|ciI9YfHmETFAp;_<^v|2amRW3`MzFmx2qRvA06ne-@9Kng5F0slS}y0a!*XM8#sVgw)umczJ?S%NHHAZxO9vO=?*9xPie z#t^SZJ6ZC&nrxlD9hSp~<(h|QedXQs^_Dh{|EYi^Z%?xsT7a` zZ>E3^`{RDcm&&vC>GiyR%Brs$os7#F9)1Ft_))x}hjG97f~?8b$qG$B0wIHf6nLou Ee?2S`p8x;= literal 0 HcmV?d00001 diff --git a/apps/wazuh-runtipi/config.json b/apps/wazuh-runtipi/config.json new file mode 100644 index 0000000..4dcc7fb --- /dev/null +++ b/apps/wazuh-runtipi/config.json @@ -0,0 +1,57 @@ +{ + "name": "Wazuh", + "id": "wazuh", + "available": true, + "exposable": true, + "dynamic_config": true, + "port": 5601, + "tipi_version": 2, + "version": "4.14.1", + "categories": ["security"], + "short_desc": "Plateforme XDR / SIEM open source.", + "description": "Wazuh est une plateforme de sécurité open source (XDR / SIEM) qui déploie la stack Docker single-node officielle : indexer, manager et dashboard Wazuh 4.14.1.", + "author": "Wazuh", + "source": "https://github.com/wazuh/wazuh-docker", + "website": "https://wazuh.com/", + "form_fields": [ + { + "type": "text", + "env_variable": "INDEXER_USERNAME", + "label": "Indexer admin username", + "default": "admin", + "required": true + }, + { + "type": "password", + "env_variable": "INDEXER_PASSWORD", + "label": "Indexer admin password", + "default": "admin", + "required": true + }, + { + "type": "text", + "env_variable": "DASHBOARD_USERNAME", + "label": "Dashboard username (internal OpenSearch user)", + "default": "kibanaserver", + "required": true + }, + { + "type": "password", + "env_variable": "DASHBOARD_PASSWORD", + "label": "Dashboard password (internal OpenSearch user)", + "default": "kibanaserver", + "required": true + }, + { + "type": "password", + "env_variable": "API_PASSWORD", + "label": "Wazuh API password (user: wazuh-wui)", + "default": "MyS3cr37P450r.*-", + "required": true + } + ], + "supported_architectures": [ + "amd64", + "arm64" + ] +} diff --git a/apps/wazuh-runtipi/data/.DS_Store b/apps/wazuh-runtipi/data/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..d7821e0dc10692ab836d27c2b1c13a9bd5dfb978 GIT binary patch literal 6148 zcmeH~F-`+P3`M^oKqQ)!lye1cFp9znxByBP5K_b zwwzEbZs++W(qXl!RuK?^uLOMdrY*ex-_U=U|2;=00wVB73D|6PyIS()vbRoN(|c{9 t-_iHR+(_pVt(Y3Em_NJ~UmfHXf98Ho9h*iu<55o49|7kg6M^3#@B!KDAcX(` literal 0 HcmV?d00001 diff --git a/apps/wazuh-runtipi/data/config/certs.yml b/apps/wazuh-runtipi/data/config/certs.yml new file mode 100644 index 0000000..0b0fe1a --- /dev/null +++ b/apps/wazuh-runtipi/data/config/certs.yml @@ -0,0 +1,12 @@ +nodes: + indexer: + - name: wazuh.indexer + ip: wazuh.indexer + server: + - name: wazuh.manager + ip: wazuh.manager + dashboard: + - name: wazuh.dashboard + ip: wazuh.dashboard + admin: + - name: admin diff --git a/apps/wazuh-runtipi/data/debug/wazuh-health-check.sh b/apps/wazuh-runtipi/data/debug/wazuh-health-check.sh new file mode 100644 index 0000000..e8a4f12 --- /dev/null +++ b/apps/wazuh-runtipi/data/debug/wazuh-health-check.sh @@ -0,0 +1,569 @@ +#!/bin/bash + +# wazuh-health-check.sh +# Script de diagnostic complet pour Wazuh sur Runtipi +# Version: 1.0 (2025-12-27) +# Usage: ./wazuh-health-check.sh + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Configuration +# Auto-detect container prefix from running containers +# Runtipi uses format: wazuh-runtipi_REPO-NAME-wazuh-SERVICE-1 +# We need to extract: wazuh-runtipi_REPO-NAME +# Note: REPO-NAME can contain hyphens (e.g., synode-it) +WAZUH_PREFIX=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -E "wazuh-runtipi.*-wazuh-" | head -1 | sed -E 's/^(.*)-wazuh-.*/\1/' || echo "wazuh-runtipi") + +# Détection automatique de l'instance dans app-data (données runtime) +# Note: app-data contient les données des conteneurs en cours d'exécution +# apps contient les fichiers sources immuables du dépôt +# Search pattern: /opt/runtipi/app-data/REPO-NAME/wazuh-runtipi/data +DATA_DIR=$(find /opt/runtipi/app-data -maxdepth 3 -type d -name "wazuh-runtipi" 2>/dev/null | head -1) +if [ -n "$DATA_DIR" ]; then + DATA_DIR="$DATA_DIR/data" +fi + +# Fallback to wildcard if find didn't work +if [ -z "$DATA_DIR" ] || [ ! -d "$DATA_DIR" ]; then + DATA_DIR=$(echo /opt/runtipi/app-data/*/wazuh-runtipi/data 2>/dev/null | awk '{print $1}') +fi + +SECURITY_DIR="$DATA_DIR/indexer-security" + +echo -e "${CYAN}=========================================${NC}" +echo -e "${CYAN} WAZUH HEALTH CHECK - $(date +%Y-%m-%d\ %H:%M:%S)${NC}" +echo -e "${CYAN}=========================================${NC}" +echo "" +echo -e "${BLUE}Configuration:${NC}" +echo -e " Container prefix: ${YELLOW}$WAZUH_PREFIX${NC}" +echo -e " Data directory: ${YELLOW}$DATA_DIR${NC}" +echo -e " Security directory: ${YELLOW}$SECURITY_DIR${NC}" +echo "" + +# Function to print section header +print_section() { + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BLUE}$1${NC}" + echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +} + +# Function to check service +check_service() { + local service=$1 + local container_name=$(docker ps -a --format '{{.Names}}' | grep -E "${WAZUH_PREFIX}.*${service}" | head -1) + + if [ -z "$container_name" ]; then + echo -e "${RED}✗ Container not found${NC}" + return 1 + fi + + local status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null) + local health=$(docker inspect --format='{{.State.Health.Status}}' "$container_name" 2>/dev/null || echo "no healthcheck") + + echo -ne " $service: " + + if [ "$status" = "running" ]; then + if [ "$health" = "healthy" ]; then + echo -e "${GREEN}✓ Running & Healthy${NC}" + return 0 + elif [ "$health" = "no healthcheck" ]; then + echo -e "${YELLOW}⚠ Running (no healthcheck)${NC}" + return 0 + else + echo -e "${YELLOW}⚠ Running but $health${NC}" + return 1 + fi + elif [ "$status" = "exited" ]; then + local exit_code=$(docker inspect --format='{{.State.ExitCode}}' "$container_name" 2>/dev/null) + if [ "$exit_code" = "0" ]; then + echo -e "${GREEN}✓ Exited successfully (code 0)${NC}" + return 0 + else + echo -e "${RED}✗ Exited with code $exit_code${NC}" + return 1 + fi + else + echo -e "${RED}✗ Status: $status${NC}" + return 1 + fi +} + +# 1. Services Health Check +print_section "1. SERVICES HEALTH CHECK" +echo "" + +SERVICES_OK=0 +SERVICES_FAILED=0 + +for service in certs indexer manager dashboard; do + if check_service "$service"; then + ((SERVICES_OK++)) + else + ((SERVICES_FAILED++)) + fi +done + +# Special check for indexer-init (runs with tail -f to stay alive - Runtipi requirement) +echo -ne " indexer-init: " +INIT_CONTAINER=$(docker ps -a --format '{{.Names}}' | grep -E "${WAZUH_PREFIX}.*indexer-init" | head -1) +if [ -n "$INIT_CONTAINER" ]; then + INIT_STATUS=$(docker inspect --format='{{.State.Status}}' "$INIT_CONTAINER" 2>/dev/null) + INIT_HEALTH=$(docker inspect --format='{{.State.Health.Status}}' "$INIT_CONTAINER" 2>/dev/null || echo "no healthcheck") + INIT_RESTARTING=$(docker inspect --format='{{.State.Restarting}}' "$INIT_CONTAINER" 2>/dev/null) + + # Check if .init-complete marker exists + INIT_COMPLETE_EXISTS=$([ -f "$SECURITY_DIR/.init-complete" ] && echo "yes" || echo "no") + + if [ "$INIT_STATUS" = "running" ] && [ "$INIT_HEALTH" = "healthy" ] && [ "$INIT_COMPLETE_EXISTS" = "yes" ]; then + echo -e "${GREEN}✓ Running & Healthy (init complete)${NC}" + ((SERVICES_OK++)) + elif [ "$INIT_RESTARTING" = "true" ] || [ "$INIT_STATUS" = "restarting" ]; then + echo -e "${RED}✗ Restarting in loop${NC}" + echo -e " ${YELLOW}⚠ This indicates a problem with security initialization${NC}" + ((SERVICES_FAILED++)) + elif [ "$INIT_STATUS" = "running" ] && [ "$INIT_COMPLETE_EXISTS" = "no" ]; then + echo -e "${YELLOW}⚠ Running but initialization not complete yet${NC}" + ((SERVICES_FAILED++)) + elif [ "$INIT_STATUS" = "running" ]; then + echo -e "${YELLOW}⚠ Running but $INIT_HEALTH${NC}" + ((SERVICES_FAILED++)) + else + echo -e "${RED}✗ Status: $INIT_STATUS${NC}" + ((SERVICES_FAILED++)) + fi +else + echo -e "${RED}✗ Container not found${NC}" + ((SERVICES_FAILED++)) +fi + +echo "" +echo -e "Summary: ${GREEN}$SERVICES_OK OK${NC} | ${RED}$SERVICES_FAILED FAILED${NC}" +echo "" + +# 1b. Container Logs for All Services +print_section "1b. CONTAINER LOGS (Last 50 lines)" +echo "" + +# Display logs for all Wazuh containers +for service in certs indexer indexer-init manager dashboard; do + container_name=$(docker ps -a --format '{{.Names}}' | grep -E "${WAZUH_PREFIX}.*${service}" | head -1) + if [ -n "$container_name" ]; then + status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null) + health=$(docker inspect --format='{{.State.Health.Status}}' "$container_name" 2>/dev/null || echo "no healthcheck") + + echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" + echo -e "${CYAN}Container: ${YELLOW}$container_name${NC}" + echo -e "${CYAN}Status: ${YELLOW}$status${NC} | Health: ${YELLOW}$health${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════${NC}" + docker logs --tail 50 "$container_name" 2>&1 | sed 's/^/ /' + echo "" + fi +done + +# 2. Disk Usage Check +print_section "2. DISK USAGE CHECK" +echo "" + +# Re-check DATA_DIR exists (in case wildcard wasn't evaluated) +if [ ! -d "$DATA_DIR" ]; then + DATA_DIR=$(find /opt/runtipi/app-data -maxdepth 2 -type d -path "*/wazuh-runtipi/data" 2>/dev/null | head -1) + SECURITY_DIR="$DATA_DIR/indexer-security" +fi + +# For disk usage, check the parent directory (wazuh-runtipi) not just /data +if [ -d "$DATA_DIR" ] && [ -n "$DATA_DIR" ]; then + # Get parent directory (remove /data from end) + APP_DIR=$(dirname "$DATA_DIR") + SIZE_HUMAN=$(du -sh "$APP_DIR" 2>/dev/null | awk '{print $1}') + SIZE_GB=$(du -sb "$APP_DIR" 2>/dev/null | awk '{print int($1/1024/1024/1024)}') + + echo -e " App directory: $APP_DIR" + echo -ne " Size: $SIZE_HUMAN ($SIZE_GB GB) - " + + if [ "$SIZE_GB" -gt 40 ]; then + echo -e "${RED}⚠ WARNING: Excessive size! Expected ~5GB${NC}" + echo -e " ${YELLOW}Possible indexer infinite loop - check Bug #1${NC}" + elif [ "$SIZE_GB" -gt 20 ]; then + echo -e "${YELLOW}⚠ INFO: Higher than expected (~5GB)${NC}" + else + echo -e "${GREEN}✓ OK (expected ~5GB)${NC}" + fi +else + echo -e "${RED}✗ Data directory not found: $DATA_DIR${NC}" +fi +echo "" + +# 3. Security Files Check +print_section "3. SECURITY FILES CHECK" +echo "" + +REQUIRED_FILES=( + "config.yml" + "roles.yml" + "roles_mapping.yml" + "internal_users.yml" + "action_groups.yml" + "tenants.yml" + "nodes_dn.yml" + "whitelist.yml" +) + +FILES_OK=0 +FILES_MISSING=0 + +# Re-check SECURITY_DIR exists +if [ ! -d "$SECURITY_DIR" ]; then + SECURITY_DIR="$DATA_DIR/indexer-security" +fi + +if [ -d "$SECURITY_DIR" ] && [ -n "$SECURITY_DIR" ]; then + echo -e " Security directory: $SECURITY_DIR" + echo "" + + for file in "${REQUIRED_FILES[@]}"; do + echo -ne " $file: " + if [ -f "$SECURITY_DIR/$file" ]; then + echo -e "${GREEN}✓ Present${NC}" + ((FILES_OK++)) + else + echo -e "${RED}✗ MISSING${NC}" + ((FILES_MISSING++)) + fi + done + + echo "" + echo -e "Summary: ${GREEN}$FILES_OK/8 files present${NC}" + + if [ "$FILES_MISSING" -gt 0 ]; then + echo -e "${RED}⚠ $FILES_MISSING files missing - Bug #4 not fixed!${NC}" + fi +else + echo -e "${RED}✗ Security directory not found: $SECURITY_DIR${NC}" +fi +echo "" + +# 4. Network Connectivity Check +print_section "4. NETWORK CONNECTIVITY CHECK" +echo "" + +DASHBOARD_CONTAINER=$(docker ps --format '{{.Names}}' | grep -E "${WAZUH_PREFIX}.*dashboard" | head -1) +INDEXER_CONTAINER=$(docker ps --format '{{.Names}}' | grep -E "${WAZUH_PREFIX}.*indexer" | grep -v "init" | head -1) + +if [ -n "$DASHBOARD_CONTAINER" ] && [ -n "$INDEXER_CONTAINER" ]; then + echo -e " Testing dashboard → indexer connectivity..." + echo "" + + # DNS resolution + echo -ne " DNS resolution (wazuh.indexer): " + if docker exec "$DASHBOARD_CONTAINER" getent hosts wazuh.indexer &>/dev/null; then + echo -e "${GREEN}✓ OK${NC}" + else + echo -e "${RED}✗ FAILED${NC}" + fi + + # HTTP connectivity + echo -ne " HTTP connectivity: " + HTTP_CODE=$(docker exec "$DASHBOARD_CONTAINER" curl -k -s -o /dev/null -w "%{http_code}" https://wazuh.indexer:9200 2>/dev/null || echo "000") + + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "401" ]; then + echo -e "${GREEN}✓ OK (HTTP $HTTP_CODE)${NC}" + elif [ "$HTTP_CODE" = "503" ]; then + echo -e "${RED}✗ FAILED (HTTP 503 - Service Unavailable)${NC}" + echo -e " ${YELLOW}This indicates Bug #4 - Security not initialized${NC}" + else + echo -e "${YELLOW}⚠ Unexpected (HTTP $HTTP_CODE)${NC}" + fi + + # Network check (containers can be on multiple networks) + echo -ne " Shared network: " + DASH_NETS=$(docker inspect "$DASHBOARD_CONTAINER" --format='{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}') + IDX_NETS=$(docker inspect "$INDEXER_CONTAINER" --format='{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}') + + # Find common networks + COMMON_NET="" + for net in $IDX_NETS; do + if echo "$DASH_NETS" | grep -q "$net"; then + COMMON_NET="$net" + break + fi + done + + if [ -n "$COMMON_NET" ]; then + echo -e "${GREEN}✓ OK ($COMMON_NET)${NC}" + else + echo -e "${RED}✗ FAILED${NC}" + echo -e " ${YELLOW}Dashboard networks: $DASH_NETS${NC}" + echo -e " ${YELLOW}Indexer networks: $IDX_NETS${NC}" + fi +else + echo -e "${RED}✗ Cannot test - dashboard or indexer not running${NC}" +fi +echo "" + +# 5. Dashboard Config Check +print_section "5. DASHBOARD CONFIGURATION CHECK" +echo "" + +if [ -n "$DASHBOARD_CONTAINER" ]; then + CONFIG_FILE="/usr/share/wazuh-dashboard/config/custom/opensearch_dashboards.yml" + + echo -ne " Config file exists: " + if docker exec "$DASHBOARD_CONTAINER" test -f "$CONFIG_FILE" 2>/dev/null; then + echo -e "${GREEN}✓ YES${NC}" + + echo -ne " Config has content: " + if docker exec "$DASHBOARD_CONTAINER" test -s "$CONFIG_FILE" 2>/dev/null; then + echo -e "${GREEN}✓ YES${NC}" + + echo -ne " opensearch.hosts configured: " + if docker exec "$DASHBOARD_CONTAINER" grep -q "opensearch.hosts:" "$CONFIG_FILE" 2>/dev/null; then + HOSTS_LINE=$(docker exec "$DASHBOARD_CONTAINER" grep "opensearch.hosts:" "$CONFIG_FILE" 2>/dev/null) + echo -e "${GREEN}✓ $HOSTS_LINE${NC}" + else + echo -e "${RED}✗ NOT FOUND${NC}" + fi + else + echo -e "${RED}✗ EMPTY${NC}" + echo -e " ${YELLOW}Dashboard config file is empty - check entrypoint script${NC}" + fi + else + echo -e "${RED}✗ NO${NC}" + echo -e " ${YELLOW}Dashboard config not created - check entrypoint script${NC}" + fi +else + echo -e "${RED}✗ Dashboard container not running${NC}" +fi +echo "" + +# 6. Manager Config Check +print_section "6. MANAGER CONFIGURATION CHECK" +echo "" + +MANAGER_CONTAINER=$(docker ps --format '{{.Names}}' | grep -E "${WAZUH_PREFIX}.*manager" | head -1) + +if [ -n "$MANAGER_CONTAINER" ]; then + OSSEC_CONF="/var/ossec/etc/ossec.conf" + OSSEC_CUSTOM="/var/ossec/etc/custom/ossec.conf" + + echo -ne " Main config exists: " + if docker exec "$MANAGER_CONTAINER" test -f "$OSSEC_CONF" 2>/dev/null; then + echo -e "${GREEN}✓ YES${NC}" + else + echo -e "${RED}✗ NO${NC}" + fi + + echo -ne " Custom config exists: " + if docker exec "$MANAGER_CONTAINER" test -f "$OSSEC_CUSTOM" 2>/dev/null; then + echo -e "${GREEN}✓ YES${NC}" + else + echo -e "${RED}✗ NO${NC}" + fi + + echo -ne " Main config is symlink: " + if docker exec "$MANAGER_CONTAINER" test -L "$OSSEC_CONF" 2>/dev/null; then + echo -e "${GREEN}✓ YES (Bug #3 fixed)${NC}" + else + echo -e "${YELLOW}⚠ NO (Bug #3 - config not persistent)${NC}" + fi +else + echo -e "${RED}✗ Manager container not running${NC}" +fi +echo "" + +# 6b. Filebeat Check +print_section "6b. FILEBEAT CHECK" +echo "" + +if [ -n "$MANAGER_CONTAINER" ]; then + FILEBEAT_CONF="/etc/filebeat/filebeat.yml" + + # Check environment variables (official Wazuh method) + echo "Environment Variables (Official Wazuh Method):" + echo "" + + echo -ne " FILEBEAT_SSL_VERIFICATION_MODE: " + SSL_VERIF=$(docker inspect "$MANAGER_CONTAINER" --format='{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep "^FILEBEAT_SSL_VERIFICATION_MODE=" | cut -d= -f2) + if [ "$SSL_VERIF" = "full" ]; then + echo -e "${GREEN}✓ full${NC}" + else + echo -e "${RED}✗ ${SSL_VERIF:-not set}${NC}" + fi + + echo -ne " SSL_CERTIFICATE_AUTHORITIES: " + SSL_CA=$(docker inspect "$MANAGER_CONTAINER" --format='{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep "^SSL_CERTIFICATE_AUTHORITIES=" | cut -d= -f2) + if [ -n "$SSL_CA" ]; then + echo -e "${GREEN}✓ ${SSL_CA}${NC}" + else + echo -e "${RED}✗ not set${NC}" + fi + + echo -ne " SSL_CERTIFICATE: " + SSL_CERT=$(docker inspect "$MANAGER_CONTAINER" --format='{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep "^SSL_CERTIFICATE=" | cut -d= -f2) + if [ -n "$SSL_CERT" ]; then + echo -e "${GREEN}✓ ${SSL_CERT}${NC}" + else + echo -e "${RED}✗ not set${NC}" + fi + + echo -ne " SSL_KEY: " + SSL_KEY=$(docker inspect "$MANAGER_CONTAINER" --format='{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep "^SSL_KEY=" | cut -d= -f2) + if [ -n "$SSL_KEY" ]; then + echo -e "${GREEN}✓ ${SSL_KEY}${NC}" + else + echo -e "${RED}✗ not set${NC}" + fi + + echo "" + echo "Generated Filebeat Configuration:" + echo "" + + echo -ne " Filebeat config exists: " + if docker exec "$MANAGER_CONTAINER" test -f "$FILEBEAT_CONF" 2>/dev/null; then + echo -e "${GREEN}✓ YES${NC}" + + echo -ne " Config has indexer https URL: " + if docker exec "$MANAGER_CONTAINER" grep -q "https://wazuh.indexer:9200" "$FILEBEAT_CONF" 2>/dev/null; then + echo -e "${GREEN}✓ YES${NC}" + else + echo -e "${RED}✗ NO (indexer URL incorrect)${NC}" + fi + + echo -ne " SSL verification enabled: " + if docker exec "$MANAGER_CONTAINER" grep -qE "ssl\.verification_mode:\s*full" "$FILEBEAT_CONF" 2>/dev/null; then + echo -e "${GREEN}✓ YES${NC}" + else + echo -e "${RED}✗ NO (SSL not configured in filebeat.yml)${NC}" + echo -e " ${YELLOW}⚠ Check if cont-init.d/1-config-filebeat ran successfully${NC}" + fi + + echo -ne " Seccomp fix for pthread: " + if docker exec "$MANAGER_CONTAINER" grep -q "seccomp:" "$FILEBEAT_CONF" 2>/dev/null; then + echo -e "${GREEN}✓ YES (pthread_create fix present)${NC}" + else + echo -e "${YELLOW}⚠ NO (may cause pthread_create errors)${NC}" + fi + else + echo -e "${RED}✗ NO${NC}" + echo -e " ${YELLOW}⚠ Filebeat config not generated - check init logs${NC}" + fi +else + echo -e "${RED}✗ Manager container not running${NC}" +fi +echo "" + +# 6c. Known Errors Detection +print_section "6c. KNOWN ERRORS DETECTION" +echo "" + +ERRORS_FOUND=0 + +if [ -n "$MANAGER_CONTAINER" ]; then + echo "Scanning manager logs for known errors..." + echo "" + + # pthread_create error + echo -ne " pthread_create error: " + if docker logs "$MANAGER_CONTAINER" 2>&1 | grep -q "pthread_create failed"; then + echo -e "${RED}✗ FOUND${NC}" + echo -e " ${YELLOW}Fix: Ensure filebeat.yml has seccomp configuration${NC}" + ((ERRORS_FOUND++)) + else + echo -e "${GREEN}✓ Not found${NC}" + fi + + # x509 certificate error + echo -ne " x509 certificate error: " + if docker logs "$MANAGER_CONTAINER" 2>&1 | grep -q "x509: certificate signed by unknown authority"; then + echo -e "${RED}✗ FOUND${NC}" + echo -e " ${YELLOW}Fix: Check SSL configuration in filebeat.yml${NC}" + ((ERRORS_FOUND++)) + else + echo -e "${GREEN}✓ Not found${NC}" + fi + + # SIGABRT crash + echo -ne " SIGABRT crash: " + if docker logs "$MANAGER_CONTAINER" 2>&1 | grep -q "SIGABRT"; then + echo -e "${RED}✗ FOUND${NC}" + echo -e " ${YELLOW}Usually caused by pthread_create error${NC}" + ((ERRORS_FOUND++)) + else + echo -e "${GREEN}✓ Not found${NC}" + fi + + # Filebeat ownership error + echo -ne " Filebeat ownership error: " + if docker logs "$MANAGER_CONTAINER" 2>&1 | grep -q "must be owned by the user identifier"; then + echo -e "${RED}✗ FOUND${NC}" + echo -e " ${YELLOW}Fix: chown root:root && chmod 600 on filebeat.yml${NC}" + ((ERRORS_FOUND++)) + else + echo -e "${GREEN}✓ Not found${NC}" + fi + + echo "" + if [ $ERRORS_FOUND -gt 0 ]; then + echo -e "${RED}Found $ERRORS_FOUND known error(s) in manager logs${NC}" + else + echo -e "${GREEN}No known errors detected in manager logs${NC}" + fi +fi +echo "" + +# 7. Final Summary +print_section "7. OVERALL HEALTH SUMMARY" +echo "" + +ISSUES=0 + +# Check services +if [ "$SERVICES_FAILED" -gt 0 ]; then + echo -e "${RED}✗ Services: $SERVICES_FAILED services have issues${NC}" + ((ISSUES++)) +else + echo -e "${GREEN}✓ Services: All services healthy${NC}" +fi + +# Check disk +if [ -n "$SIZE_GB" ] && [ "$SIZE_GB" -gt 20 ]; then + echo -e "${YELLOW}⚠ Disk: Higher than expected usage ($SIZE_GB GB)${NC}" + ((ISSUES++)) +elif [ -n "$SIZE_GB" ]; then + echo -e "${GREEN}✓ Disk: Usage normal (~5GB)${NC}" +else + echo -e "${YELLOW}⚠ Disk: Could not determine usage${NC}" +fi + +# Check security files +if [ "$FILES_MISSING" -gt 0 ]; then + echo -e "${RED}✗ Security: $FILES_MISSING files missing${NC}" + ((ISSUES++)) +else + echo -e "${GREEN}✓ Security: All 8 security files present${NC}" +fi + +echo "" +echo -e "${CYAN}=========================================${NC}" + +if [ "$ISSUES" -eq 0 ]; then + echo -e "${GREEN}✓✓✓ WAZUH IS HEALTHY - PRODUCTION READY ✓✓✓${NC}" +else + echo -e "${YELLOW}⚠⚠⚠ FOUND $ISSUES ISSUE(S) - CHECK ABOVE ⚠⚠⚠${NC}" + echo "" + echo -e "${CYAN}Troubleshooting:${NC}" + echo " 1. Check logs above for detailed error messages" + echo " 2. See metadata/description.md section 'TROUBLESHOOTING'" + echo " 3. Verify docker-compose.json entrypoints are correct" + echo " 4. Check container prefix detection: $WAZUH_PREFIX" +fi + +echo -e "${CYAN}=========================================${NC}" +echo "" + +exit 0 diff --git a/apps/wazuh-runtipi/data/scripts/init-certs.sh b/apps/wazuh-runtipi/data/scripts/init-certs.sh new file mode 100644 index 0000000..a084240 --- /dev/null +++ b/apps/wazuh-runtipi/data/scripts/init-certs.sh @@ -0,0 +1,72 @@ +#!/bin/sh +set -e + +echo "CERTS_INIT: Starting certificate initialization..." + +# Create all required directories +echo "CERTS_INIT: Creating directories..." +mkdir -p /indexer-data \ + /manager-api \ + /manager-logs \ + /manager-queue \ + /dashboard-config \ + /indexer-security + +# Super-Janitor Sweep: Remove any files/directories that were incorrectly created as directories +echo "CERTS_INIT: Starting Super-Janitor Sweep..." +for path in /certificates/*.pem \ + /certificates/*.key \ + /dashboard-config/opensearch_dashboards.yml \ + /indexer-security/config.yml \ + /indexer-security/nodes_dn.yml \ + /indexer-security/tenants.yml \ + /indexer-security/whitelist.yml \ + /indexer-security/roles.yml \ + /indexer-security/roles_mapping.yml \ + /indexer-security/internal_users.yml \ + /indexer-security/action_groups.yml; do + if [ -d "$path" ]; then + echo "CERTS_INIT: Purging fake directory: $path" + rm -rf "$path" + fi +done + +# Generate certificates if they don't exist +if [ ! -f /certificates/root-ca.pem ]; then + echo "CERTS_INIT: Generating new certificates..." + /entrypoint.sh +else + echo "CERTS_INIT: Certificates already exist, skipping generation" +fi + +# Create symlinks for easier reference +echo "CERTS_INIT: Creating certificate symlinks..." +ln -sf wazuh.indexer.pem /certificates/indexer.pem +ln -sf wazuh.indexer-key.pem /certificates/indexer-key.pem +ln -sf wazuh.manager.pem /certificates/server.pem +ln -sf wazuh.manager-key.pem /certificates/server-key.pem +ln -sf wazuh.dashboard.pem /certificates/dashboard.pem +ln -sf wazuh.dashboard-key.pem /certificates/dashboard-key.pem + +# Set correct ownership +# - 1000:1000 for indexer and dashboard (opensearch/kibana user) +# - 999:999 for manager directories (wazuh user in manager container) +echo "CERTS_INIT: Setting ownership and permissions..." +chown -R 1000:1000 /certificates \ + /indexer-data \ + /dashboard-config \ + /indexer-security + +chown -R 999:999 /manager-api \ + /manager-logs \ + /manager-queue + +# Set correct permissions +chmod 700 /certificates +chmod 644 /certificates/*.pem 2>/dev/null || true +chmod 600 /certificates/*.key 2>/dev/null || true + +echo "CERTS_INIT: Certificates ready" + +# Keep container alive (Runtipi requirement) +tail -f /dev/null diff --git a/apps/wazuh-runtipi/data/scripts/init-dashboard.sh b/apps/wazuh-runtipi/data/scripts/init-dashboard.sh new file mode 100644 index 0000000..4b21015 --- /dev/null +++ b/apps/wazuh-runtipi/data/scripts/init-dashboard.sh @@ -0,0 +1,134 @@ +#!/bin/bash +set -e + +echo "DASHBOARD_INIT: Starting dashboard initialization..." + +CUSTOM_CONFIG="/usr/share/wazuh-dashboard/config/custom/opensearch_dashboards.yml" + +# Ensure custom directory exists +echo "DASHBOARD_INIT: Ensuring custom config directory exists..." +mkdir -p /usr/share/wazuh-dashboard/config/custom + +# Check if custom config exists, if not create default +if [ ! -s "$CUSTOM_CONFIG" ]; then + echo "DASHBOARD_INIT: Creating default dashboard config..." + + cat > "$CUSTOM_CONFIG" << EOF +server.host: 0.0.0.0 +server.port: 5601 +opensearch.hosts: https://wazuh.indexer:9200 +opensearch.ssl.verificationMode: certificate +opensearch.username: ${DASHBOARD_USERNAME:-kibanaserver} +opensearch.password: ${DASHBOARD_PASSWORD:-kibanaserver} +opensearch.requestHeadersWhitelist: ["securitytenant","Authorization"] +opensearch_security.multitenancy.enabled: false +opensearch_security.readonly_mode.roles: ["kibana_read_only"] +server.ssl.enabled: true +server.ssl.certificate: /usr/share/wazuh-dashboard/config/certs/dashboard.pem +server.ssl.key: /usr/share/wazuh-dashboard/config/certs/dashboard-key.pem +opensearch.ssl.certificateAuthorities: ["/usr/share/wazuh-dashboard/config/certs/root-ca.pem"] +uiSettings.overrides.defaultRoute: /app/wazuh +EOF + + echo "DASHBOARD_INIT: Default dashboard config created" +else + echo "DASHBOARD_INIT: Custom dashboard config already exists, skipping" +fi + +# Create symlink if it doesn't exist +if [ ! -L /usr/share/wazuh-dashboard/config/opensearch_dashboards.yml ]; then + echo "DASHBOARD_INIT: Creating symlink to custom config..." + rm -f /usr/share/wazuh-dashboard/config/opensearch_dashboards.yml + ln -s "$CUSTOM_CONFIG" /usr/share/wazuh-dashboard/config/opensearch_dashboards.yml +else + echo "DASHBOARD_INIT: Symlink already exists" +fi + +# Handle keystore initialization idempotently to avoid interactive prompts +KEYSTORE_PATH="/usr/share/wazuh-dashboard/config/opensearch_dashboards.keystore" +if [ ! -f "$KEYSTORE_PATH" ]; then + echo "DASHBOARD_INIT: Creating dashboard keystore..." + # The --allow-root flag is often needed if running as root + /usr/share/wazuh-dashboard/bin/opensearch-dashboards-keystore create 2>/dev/null || true +fi + +# Start dashboard in background +echo "DASHBOARD_INIT: Configuration complete, starting dashboard..." +/entrypoint.sh & +DASHBOARD_PID=$! + +# Give the process a moment to start and resolve its sub-processes +sleep 5 + +# Robust PID tracking: looking for the actual Node.js process if entrypoint isn't using exec +if ! kill -0 $DASHBOARD_PID 2>/dev/null; then + echo "DASHBOARD_INIT: Main PID $DASHBOARD_PID died, checking for sub-processes..." + # Look for opensearch-dashboards node process + ACTUAL_PID=$(pgrep -f "opensearch-dashboards" || echo "") + if [ -n "$ACTUAL_PID" ]; then + DASHBOARD_PID=$ACTUAL_PID + echo "DASHBOARD_INIT: Found actual dashboard process at PID $DASHBOARD_PID" + else + echo "DASHBOARD_INIT: ERROR - No dashboard process found!" + exit 1 + fi +fi + +# Periodic monitoring with migration lock detection +echo "MIGRATION_WATCHDOG: Monitoring dashboard startup (PID: $DASHBOARD_PID)..." +TIMEOUT=600 # Increase timeout to 10 minutes due to Indexer slowness +ELAPSED=0 +MIGRATION_LOCK_DETECTED_AT=0 + +while [ $ELAPSED -lt $TIMEOUT ]; do + # Check if dashboard process is still alive + if ! kill -0 $DASHBOARD_PID 2>/dev/null; then + echo "MIGRATION_WATCHDOG: ERROR - Dashboard process (PID: $DASHBOARD_PID) died after ${ELAPSED}s!" + exit 1 + fi + + # Check if dashboard API is responding + API_STATUS=$(curl -sk -o /dev/null -w "%{http_code}" https://localhost:5601/api/status 2>/dev/null) + if [[ "$API_STATUS" == "200" || "$API_STATUS" == "302" || "$API_STATUS" == "401" ]]; then + echo "MIGRATION_WATCHDOG: Dashboard is responding (HTTP $API_STATUS) after ${ELAPSED}s" + # We don't break yet, we continue monitoring until it's "Stable" (HTTP 200) + if [ "$API_STATUS" == "200" ]; then + echo "MIGRATION_WATCHDOG: Dashboard is fully UP and Stable." + break + fi + fi + + # Check for migration lock (stuck .kibana_1) + HTTP_CODE=$(curl -sk -u "${INDEXER_USERNAME:-admin}:${INDEXER_PASSWORD:-admin}" -o /dev/null -w "%{http_code}" "https://wazuh.indexer:9200/.kibana_1" 2>/dev/null) + + if [ "$HTTP_CODE" = "200" ] && [ $MIGRATION_LOCK_DETECTED_AT -eq 0 ]; then + MIGRATION_LOCK_DETECTED_AT=$ELAPSED + echo "MIGRATION_WATCHDOG: Detected .kibana_1 index at ${ELAPSED}s, waiting for natural migration..." + fi + + if [ "$HTTP_CODE" = "200" ] && [ $MIGRATION_LOCK_DETECTED_AT -gt 0 ]; then + STUCK_DURATION=$((ELAPSED - MIGRATION_LOCK_DETECTED_AT)) + if [ $STUCK_DURATION -ge 240 ]; then # Increase to 4 minutes + echo "MIGRATION_WATCHDOG: Index stuck for ${STUCK_DURATION}s, cleaning up..." + curl -sk -u "${INDEXER_USERNAME:-admin}:${INDEXER_PASSWORD:-admin}" -X DELETE "https://wazuh.indexer:9200/.kibana_1" 2>/dev/null || true + kill $DASHBOARD_PID 2>/dev/null || true + exit 0 + fi + fi + + sleep 15 + ELAPSED=$((ELAPSED + 15)) + + if [ $((ELAPSED % 60)) -eq 0 ]; then + echo "MIGRATION_WATCHDOG: Still waiting for dashboard (${ELAPSED}s, Status: $API_STATUS)..." + fi +done + +echo "MIGRATION_WATCHDOG: Finalizing monitoring. Entering persistent loop." +# Keep watching the dashboard process +while kill -0 $DASHBOARD_PID 2>/dev/null; do + sleep 60 +done + +echo "DASHBOARD_INIT: Process died. Exiting." +exit 1 diff --git a/apps/wazuh-runtipi/data/scripts/init-indexer-init.sh b/apps/wazuh-runtipi/data/scripts/init-indexer-init.sh new file mode 100644 index 0000000..700f320 --- /dev/null +++ b/apps/wazuh-runtipi/data/scripts/init-indexer-init.sh @@ -0,0 +1,56 @@ +#!/bin/bash +set -e + +echo "INDEXER_INIT: Starting security initialization..." + +# Check if security files already exist +if [ ! -f /mnt/host-security/internal_users.yml ]; then + echo "INDEXER_INIT: Copying security configs..." + + SRC_PATH="/usr/share/wazuh-indexer/config/opensearch-security" + + for file in config.yml roles.yml roles_mapping.yml internal_users.yml action_groups.yml tenants.yml nodes_dn.yml whitelist.yml; do + if [ -f "$SRC_PATH/$file" ]; then + cp "$SRC_PATH/$file" /mnt/host-security/ + echo "INDEXER_INIT: Copied $file" + else + echo "INDEXER_INIT: $file not found, skipping" + fi + done + + echo "INDEXER_INIT: Security files ready" +else + echo "INDEXER_INIT: Security files already exist, skipping copy" +fi + +# Set JAVA_HOME +export JAVA_HOME=/usr/share/wazuh-indexer/jdk + +# Wait for indexer to be ready +echo "INDEXER_INIT: Waiting for indexer to be available..." +until curl -ks https://wazuh.indexer:9200 -u "${INDEXER_USERNAME:-admin}:${INDEXER_PASSWORD:-admin}"; do + echo "INDEXER_INIT: Indexer not ready, retrying in 5 seconds..." + sleep 5 +done + +echo "INDEXER_INIT: Indexer is ready, initializing security..." + +# Initialize security +/usr/share/wazuh-indexer/plugins/opensearch-security/tools/securityadmin.sh \ + -cd /mnt/host-security/ \ + -cacert /usr/share/wazuh-indexer/config/certs/root-ca.pem \ + -cert /usr/share/wazuh-indexer/config/certs/admin.pem \ + -key /usr/share/wazuh-indexer/config/certs/admin-key.pem \ + -h wazuh.indexer \ + -p 9200 \ + -nhnv + +echo "INDEXER_INIT: Security initialization completed successfully" + +# Create completion marker file +touch /mnt/host-security/.init-complete + +# Keep container alive (Runtipi requirement) +# Using tail -f /dev/null keeps the container in a healthy "running" state +echo "INDEXER_INIT: Initialization complete, container will remain alive" +tail -f /dev/null diff --git a/apps/wazuh-runtipi/data/scripts/init-manager.sh b/apps/wazuh-runtipi/data/scripts/init-manager.sh new file mode 100644 index 0000000..66f019c --- /dev/null +++ b/apps/wazuh-runtipi/data/scripts/init-manager.sh @@ -0,0 +1,100 @@ +#!/bin/bash +set -e + +echo "MANAGER_INIT: Starting manager initialization..." + +# ============================================================================ +# OSSEC.CONF CONFIGURATION +# ============================================================================ +# The official Wazuh /init script creates ossec.conf during initialization. +# We use a watchdog to copy it to custom storage for persistence after /init. + +OSSEC_CUSTOM="/var/ossec/etc/custom/ossec.conf" +OSSEC_DEFAULT="/var/ossec/etc/ossec.conf" + +# Create custom directory if it doesn't exist +mkdir -p /var/ossec/etc/custom + +# NOTE: Filebeat SSL configuration is now handled via environment variables: +# - FILEBEAT_SSL_VERIFICATION_MODE=full +# - SSL_CERTIFICATE_AUTHORITIES=/var/ossec/etc/certs/root-ca.pem +# - SSL_CERTIFICATE=/var/ossec/etc/certs/server.pem +# - SSL_KEY=/var/ossec/etc/certs/server-key.pem +# The official cont-init.d/1-config-filebeat script will generate the correct +# configuration automatically. No manual filebeat.yml management needed! + +# ============================================================================ +# POST-INIT WATCHDOG +# ============================================================================ +# The Wazuh /init script creates ossec.conf during initialization. +# This watchdog waits for init completion, then makes ossec.conf persistent. + +( + echo "WATCHDOG: Waiting for Wazuh services to be fully started..." + + # Wait for wazuh-db to be running (not just starting) + # wazuh-db is one of the last services to start and needs a valid ossec.conf + TIMEOUT=180 + ELAPSED=0 + while [ $ELAPSED -lt $TIMEOUT ]; do + # Check if wazuh-db process is running + if pgrep -x "wazuh-db" > /dev/null 2>&1; then + echo "WATCHDOG: wazuh-db is running, waiting additional 5s for stability..." + sleep 5 + break + fi + sleep 2 + ELAPSED=$((ELAPSED + 2)) + + if [ $((ELAPSED % 20)) -eq 0 ]; then + echo "WATCHDOG: Still waiting for wazuh-db to start (${ELAPSED}s elapsed)..." + fi + done + + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "WATCHDOG: WARNING - Timeout waiting for wazuh-db startup!" + echo "WATCHDOG: Will proceed anyway, but persistence may fail" + fi + + # Now make ossec.conf persistent + if [ -f "$OSSEC_DEFAULT" ] && [ ! -L "$OSSEC_DEFAULT" ]; then + echo "WATCHDOG: Making ossec.conf persistent..." + + # If custom file doesn't exist or is empty, copy current to custom + if [ ! -s "$OSSEC_CUSTOM" ]; then + echo "WATCHDOG: Backing up current ossec.conf to custom storage..." + cp "$OSSEC_DEFAULT" "$OSSEC_CUSTOM" + fi + + # Create symlink for persistence + echo "WATCHDOG: Creating symlink /var/ossec/etc/ossec.conf -> custom/ossec.conf" + rm -f "$OSSEC_DEFAULT" + ln -s "$OSSEC_CUSTOM" "$OSSEC_DEFAULT" + + # Verify symlink was created + if [ -L "$OSSEC_DEFAULT" ]; then + echo "WATCHDOG: ✓ ossec.conf is now persistent (symlink verified)" + else + echo "WATCHDOG: ✗ ERROR - Failed to create symlink!" + fi + else + echo "WATCHDOG: ossec.conf already persistent (symlink exists)" + fi + + echo "WATCHDOG: Initialization complete, entering monitoring mode" + + # Keep watchdog alive + while true; do + sleep 3600 + done +) & + +# ============================================================================ +# START WAZUH +# ============================================================================ +echo "MANAGER_INIT: Configuration complete, starting Wazuh..." + +# Execute the original Wazuh entrypoint +# The cont-init.d/1-config-filebeat script will automatically configure Filebeat +# using the SSL environment variables we defined in docker-compose.json +exec /init diff --git a/apps/wazuh-runtipi/docker-compose.json b/apps/wazuh-runtipi/docker-compose.json new file mode 100644 index 0000000..d982811 --- /dev/null +++ b/apps/wazuh-runtipi/docker-compose.json @@ -0,0 +1,406 @@ +{ + "schemaVersion": 2, + "services": [ + { + "name": "wazuh-certs", + "image": "wazuh/wazuh-certs-generator:0.0.3", + "hostname": "wazuh-certs-generator", + "entrypoint": [ + "sh", + "/scripts/init-certs.sh" + ], + "environment": [ + { + "key": "CERT_TOOL_VERSION", + "value": "4.14" + } + ], + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/config", + "containerPath": "/config" + }, + { + "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", + "containerPath": "/certificates" + }, + { + "hostPath": "${APP_DATA_DIR}/data/indexer-data", + "containerPath": "/indexer-data" + }, + { + "hostPath": "${APP_DATA_DIR}/data/manager-api", + "containerPath": "/manager-api" + }, + { + "hostPath": "${APP_DATA_DIR}/data/manager-logs", + "containerPath": "/manager-logs" + }, + { + "hostPath": "${APP_DATA_DIR}/data/manager-queue", + "containerPath": "/manager-queue" + }, + { + "hostPath": "${APP_DATA_DIR}/data/dashboard-config", + "containerPath": "/dashboard-config" + }, + { + "hostPath": "${APP_DATA_DIR}/data/indexer-security", + "containerPath": "/indexer-security" + }, + { + "hostPath": "${APP_DATA_DIR}/data/scripts", + "containerPath": "/scripts" + } + ], + "healthCheck": { + "test": "test -f /certificates/root-ca.pem", + "interval": "5s", + "timeout": "5s", + "retries": 10 + } + }, + { + "name": "wazuh-indexer", + "image": "wazuh/wazuh-indexer:4.14.1", + "hostname": "wazuh.indexer", + "user": "1000:1000", + "depends_on": { + "wazuh-certs": { + "condition": "service_healthy" + } + }, + "environment": [ + { + "key": "OPENSEARCH_JAVA_OPTS", + "value": "-Xms1g -Xmx1g" + }, + { + "key": "bootstrap.memory_lock", + "value": "true" + }, + { + "key": "network.host", + "value": "wazuh.indexer" + }, + { + "key": "node.name", + "value": "wazuh.indexer" + }, + { + "key": "cluster.initial_cluster_manager_nodes", + "value": "wazuh.indexer" + }, + { + "key": "node.max_local_storage_nodes", + "value": "1" + }, + { + "key": "plugins.security.allow_default_init_securityindex", + "value": "true" + }, + { + "key": "NODES_DN", + "value": "CN=wazuh.indexer,OU=Wazuh,O=Wazuh,L=California,C=US" + }, + { + "key": "plugins.security.ssl.http.clientauth_mode", + "value": "OPTIONAL" + } + ], + "ulimits": { + "memlock": { + "soft": -1, + "hard": -1 + }, + "nofile": { + "soft": 65536, + "hard": 65536 + } + }, + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/indexer-data", + "containerPath": "/var/lib/wazuh-indexer" + }, + { + "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", + "containerPath": "/usr/share/wazuh-indexer/config/certs" + }, + { + "hostPath": "${APP_DATA_DIR}/data/indexer-security", + "containerPath": "/usr/share/wazuh-indexer/opensearch-security" + } + ], + "healthCheck": { + "test": "curl -ks https://wazuh.indexer:9200 -u ${INDEXER_USERNAME:-admin}:${INDEXER_PASSWORD:-admin}", + "interval": "10s", + "timeout": "5s", + "retries": 15, + "startPeriod": "120s" + } + }, + { + "name": "wazuh-indexer-init", + "image": "wazuh/wazuh-indexer:4.14.1", + "hostname": "wazuh-indexer-init", + "depends_on": { + "wazuh-indexer": { + "condition": "service_healthy" + } + }, + "entrypoint": [ + "bash", + "/scripts/init-indexer-init.sh" + ], + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", + "containerPath": "/usr/share/wazuh-indexer/config/certs" + }, + { + "hostPath": "${APP_DATA_DIR}/data/indexer-security", + "containerPath": "/mnt/host-security" + }, + { + "hostPath": "${APP_DATA_DIR}/data/scripts", + "containerPath": "/scripts" + } + ], + "healthCheck": { + "test": "test -f /mnt/host-security/.init-complete", + "interval": "5s", + "timeout": "5s", + "retries": 60, + "startPeriod": "120s" + } + }, + { + "name": "wazuh-manager", + "image": "wazuh/wazuh-manager:4.14.1", + "hostname": "wazuh.manager", + "entrypoint": [ + "bash", + "/scripts/init-manager.sh" + ], + "depends_on": { + "wazuh-indexer-init": { + "condition": "service_healthy" + } + }, + "environment": [ + { + "key": "WAZUH_INDEXER_HOSTS", + "value": "wazuh.indexer:9200" + }, + { + "key": "WAZUH_NODE_NAME", + "value": "manager" + }, + { + "key": "WAZUH_CLUSTER_NODES", + "value": "wazuh.manager" + }, + { + "key": "WAZUH_CLUSTER_BIND_ADDR", + "value": "wazuh.manager" + }, + { + "key": "INDEXER_URL", + "value": "https://wazuh.indexer:9200" + }, + { + "key": "INDEXER_USERNAME", + "value": "${INDEXER_USERNAME:-admin}" + }, + { + "key": "INDEXER_PASSWORD", + "value": "${INDEXER_PASSWORD:-admin}" + }, + { + "key": "FILEBEAT_SSL_VERIFICATION_MODE", + "value": "full" + }, + { + "key": "SSL_CERTIFICATE_AUTHORITIES", + "value": "/var/ossec/etc/certs/root-ca.pem" + }, + { + "key": "SSL_CERTIFICATE", + "value": "/var/ossec/etc/certs/server.pem" + }, + { + "key": "SSL_KEY", + "value": "/var/ossec/etc/certs/server-key.pem" + }, + { + "key": "API_USERNAME", + "value": "wazuh-wui" + }, + { + "key": "API_PASSWORD", + "value": "${API_PASSWORD:-MyS3cr37P450r.*-}" + } + ], + "ulimits": { + "memlock": { + "soft": -1, + "hard": -1 + }, + "nofile": { + "soft": 655360, + "hard": 655360 + } + }, + "addPorts": [ + { + "containerPort": 1514, + "hostPort": 1514, + "tcp": true + }, + { + "containerPort": 1515, + "hostPort": 1515, + "tcp": true + }, + { + "containerPort": 514, + "hostPort": 514, + "udp": true + }, + { + "containerPort": 55000, + "hostPort": 55000, + "tcp": true + } + ], + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/manager-api", + "containerPath": "/var/ossec/api/configuration" + }, + { + "hostPath": "${APP_DATA_DIR}/data/manager-etc", + "containerPath": "/var/ossec/etc/custom" + }, + { + "hostPath": "${APP_DATA_DIR}/data/manager-logs", + "containerPath": "/var/ossec/logs" + }, + { + "hostPath": "${APP_DATA_DIR}/data/manager-queue", + "containerPath": "/var/ossec/queue" + }, + { + "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", + "containerPath": "/var/ossec/etc/certs" + }, + { + "hostPath": "${APP_DATA_DIR}/data/scripts", + "containerPath": "/scripts" + } + ], + "healthCheck": { + "test": "/var/ossec/bin/wazuh-control status | grep -E 'wazuh-db is running|wazuh-analysisd is running|wazuh-remoted is running' | wc -l | grep -q 3", + "interval": "30s", + "timeout": "10s", + "retries": 10, + "startPeriod": "180s" + } + }, + { + "name": "wazuh-dashboard", + "image": "wazuh/wazuh-dashboard:4.14.1", + "hostname": "wazuh.dashboard", + "entrypoint": [ + "bash", + "/scripts/init-dashboard.sh" + ], + "isMain": true, + "internalPort": "5601", + "depends_on": { + "wazuh-indexer-init": { + "condition": "service_healthy" + }, + "wazuh-manager": { + "condition": "service_healthy" + } + }, + "environment": [ + { + "key": "SERVER_HOST", + "value": "0.0.0.0" + }, + { + "key": "OPENSEARCH_HOSTS", + "value": "https://wazuh.indexer:9200" + }, + { + "key": "INDEXER_USERNAME", + "value": "${INDEXER_USERNAME:-admin}" + }, + { + "key": "INDEXER_PASSWORD", + "value": "${INDEXER_PASSWORD:-admin}" + }, + { + "key": "WAZUH_API_URL", + "value": "https://wazuh.manager" + }, + { + "key": "DASHBOARD_USERNAME", + "value": "${DASHBOARD_USERNAME:-kibanaserver}" + }, + { + "key": "DASHBOARD_PASSWORD", + "value": "${DASHBOARD_PASSWORD:-kibanaserver}" + }, + { + "key": "API_USERNAME", + "value": "wazuh-wui" + }, + { + "key": "API_PASSWORD", + "value": "${API_PASSWORD:-MyS3cr37P450r.*-}" + }, + { + "key": "SERVER_SSL_CERTIFICATE", + "value": "/usr/share/wazuh-dashboard/config/certs/dashboard.pem" + }, + { + "key": "SERVER_SSL_KEY", + "value": "/usr/share/wazuh-dashboard/config/certs/dashboard-key.pem" + }, + { + "key": "OPENSEARCH_SSL_CERTIFICATE_AUTHORITIES", + "value": "/usr/share/wazuh-dashboard/config/certs/root-ca.pem" + } + ], + "volumes": [ + { + "hostPath": "${APP_DATA_DIR}/data/config/wazuh_ssl_certs", + "containerPath": "/usr/share/wazuh-dashboard/config/certs" + }, + { + "hostPath": "${APP_DATA_DIR}/data/dashboard-config", + "containerPath": "/usr/share/wazuh-dashboard/config/custom" + }, + { + "hostPath": "${APP_DATA_DIR}/data/dashboard-custom", + "containerPath": "/usr/share/wazuh-dashboard/plugins/wazuh/public/assets/custom" + }, + { + "hostPath": "${APP_DATA_DIR}/data/scripts", + "containerPath": "/scripts" + } + ], + "healthCheck": { + "test": "curl -ks https://localhost:5601/app/wazuh -o /dev/null -w '%{http_code}' | grep -qE '302|200' || exit 1", + "interval": "30s", + "timeout": "10s", + "retries": 15, + "startPeriod": "240s" + } + } + ] +} \ No newline at end of file diff --git a/apps/wazuh-runtipi/metadata/description.md b/apps/wazuh-runtipi/metadata/description.md new file mode 100644 index 0000000..5efce0d --- /dev/null +++ b/apps/wazuh-runtipi/metadata/description.md @@ -0,0 +1,762 @@ +# Wazuh 4.14.1 pour Runtipi + +Déploiement de Wazuh, la plateforme open-source de sécurité unifiée (XDR et SIEM), sur Runtipi. + +**Version:** Wazuh 4.14.1 +**Plateforme:** Runtipi 4.6.5 +**Status:** ✅ Stable + +⚠️ **IMPORTANT SÉCURITÉ:** Les mots de passe par défaut DOIVENT être changés avant la mise en production. Voir la section "Configuration des Identifiants" ci-dessous. + +--- + +## 📖 Qu'est-ce que Wazuh ? + +**Wazuh** est une plateforme de sécurité open-source qui fournit: + +- **SIEM (Security Information and Event Management)** - Centralisation et analyse des logs de sécurité +- **XDR (Extended Detection and Response)** - Détection et réponse aux menaces +- **Conformité** - Vérification de conformité (PCI DSS, HIPAA, GDPR, etc.) +- **Détection des vulnérabilités** - Scan et gestion des vulnérabilités +- **Détection d'intrusion** - Monitoring temps réel des fichiers système +- **Réponse aux incidents** - Automatisation des réponses de sécurité + +### Ressources Officielles + +- **Site Web:** https://wazuh.com/ +- **Documentation:** https://documentation.wazuh.com/current/ +- **GitHub Wazuh:** https://github.com/wazuh/wazuh +- **Wazuh Docker:** https://github.com/wazuh/wazuh-docker +- **Community:** https://groups.google.com/g/wazuh + +--- + +## 🏗️ Architecture + +Ce projet déploie une stack Wazuh complète sur Runtipi avec 5 conteneurs Docker: + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Votre Infrastructure │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Serveur │ │ Serveur │ │ Desktop │ │ Cloud │ │ +│ │ Linux │ │ Windows │ │ MacOS │ │ Instance │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ │ +│ │ Wazuh Agents (1514 / 1515) │ │ +│ └──────────────┬──────────────┬──────────────┘ │ +└───────────────────────┼──────────────┼───────────────────────┘ + ▼ │ +┌───────────────────────┼──────────────┼───────────────────────┐ +│ Stack Wazuh (Runtipi) │ +│ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Wazuh Manager (Ports 1514 / 1515 / 55000) │ │ +│ │ - Collecte des événements │ │ +│ │ - Analyse & corrélation │ │ +│ │ - Règles de détection │ │ +│ │ - API REST │ │ +│ └────────────────────┬───────────────────────────────────┘ │ +│ │ (Filebeat) │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Wazuh Indexer (OpenSearch - Port 9200) │ │ +│ │ - Stockage & indexation │ │ +│ │ - Recherches rapides │ │ +│ │ - Statistiques / agrégations │ │ +│ └────────────────────┬───────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ Wazuh Dashboard (Port 5601) - Interface Web │ │ +│ │ - Visualisation des alertes │ │ +│ │ - Tableaux de bord │ │ +│ │ - Gestion des agents │ │ +│ │ - Configuration / Admin │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ +│ Services additionnels : │ +│ • wazuh-certs : génération certificats SSL/TLS │ +│ • wazuh-indexer-init : initialisation sécurité OpenSearch │ +└──────────────────────────────────────────────────────────────┘ + │ + ▼ + Accès via navigateur + https://VOTRE_IP:5601 +``` + +### Composants + +1. **Wazuh Manager** - Analyse les événements de sécurité collectés par les agents +2. **Wazuh Indexer** - Base de données OpenSearch pour stocker et indexer les événements +3. **Wazuh Dashboard** - Interface web pour visualiser et gérer la sécurité +4. **Wazuh Certs Generator** - Génère les certificats SSL/TLS pour les communications sécurisées +5. **Wazuh Indexer Init** - Initialise la configuration de sécurité d'OpenSearch + +--- + +## 🚀 Installation + +### Prérequis + +- **Runtipi 4.6.5+** installé et fonctionnel +- **4GB RAM minimum** (8GB recommandés) +- **20GB espace disque** disponible +- **Accès root** au serveur Runtipi + +**⚠️ IMPORTANT - Configuration système requise:** + +Avant l'installation, exécutez cette commande sur votre serveur: + +```bash +sudo sysctl -w vm.max_map_count=262144 +echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf +``` + +Cette configuration est **obligatoire** pour le bon fonctionnement d'OpenSearch (le moteur d'indexation de Wazuh). Sans elle, le conteneur `wazuh-indexer` ne démarrera pas correctement. + +### Étape 1: Installer via Runtipi + +1. **Ouvrir l'interface Runtipi** dans votre navigateur +2. **Aller dans "App Store"** +3. **Rechercher "Wazuh"** +4. **Cliquer sur "Install"** +5. **⚠️ IMPORTANT : Attendre 5 minutes complètes.** + +> [!WARNING] +> Le déploiement de Wazuh est lourd. Le service `wazuh-dashboard` peut redémarrer plusieurs fois pendant les 5 premières minutes le temps que l'indexeur soit prêt. +> Si vous voyez le statut "Rebooting" ou "Starting", **ne touchez à rien** et patientez. C'est le comportement normal du script de surveillance (Watchdog). + +C'est tout! L'installation est **100% automatique** ✅ + +### Étape 2: Vérifier l'Installation + +Connectez-vous en SSH au serveur Runtipi: + +```bash +# Vérifier que tous les services sont démarrés et healthy +docker ps -a | grep wazuh +``` + +**Résultat attendu:** + +Vous devriez voir 5 conteneurs: +- ✅ **wazuh-certs** - Up X min (healthy) +- ✅ **wazuh-indexer** - Up X min (healthy) +- ✅ **wazuh-indexer-init** - Up X min (healthy) +- ✅ **wazuh-manager** - Up X min (healthy) +- ✅ **wazuh-dashboard** - Up X min (healthy) + +**Note:** Le conteneur `wazuh-indexer-init` reste running avec un health check (adaptation Runtipi). Il initialise la sécurité OpenSearch au démarrage, crée un fichier marker `.init-complete`, puis reste en veille. Le statut "healthy" confirme que l'initialisation est terminée. + +### Étape 3: Accéder au Dashboard + +1. **Ouvrir votre navigateur** +2. **Aller à:** `https://VOTRE_IP_SERVEUR:5601` +3. **Se connecter avec les credentials par défaut** + +## 🔐 Configuration des Identifiants + +### Variables d'Environnement + +Lors de l'installation via Runtipi, vous pouvez configurer les identifiants dans l'interface GUI: + +| Variable | Description | Valeur par défaut | +|----------------------|------------------------------------------------|----------------------| +| `INDEXER_USERNAME` | Nom d'utilisateur admin de l'indexer | `admin` | +| `INDEXER_PASSWORD` | Mot de passe admin de l'indexer | `admin` | +| `DASHBOARD_USERNAME` | Utilisateur interne dashboard→indexer | `kibanaserver` | +| `DASHBOARD_PASSWORD` | Mot de passe interne dashboard→indexer | `kibanaserver` | +| `API_PASSWORD` | Mot de passe de l'API REST (user: wazuh-wui) | `MyS3cr37P450r.*-` | + +### Credentials par Défaut + +Si vous utilisez les valeurs par défaut: + +| Service | Username | Password | Usage | +|---------------|----------------|----------------------|--------------------------------| +| Dashboard | `admin` | `admin` | Interface web principale | +| API | `wazuh-wui` | `MyS3cr37P450r.*-` | API REST du manager | +| Kibanaserver | `kibanaserver` | `kibanaserver` | Connexion dashboard→indexer | + +**⚠️ IMPORTANT:** Pour une utilisation en production, modifiez ces mots de passe **avant** l'installation via l'interface Runtipi! + +### Étape 4: Changer les Mots de Passe (Recommandé pour Production) + +#### Méthode 1: Avant l'Installation (Recommandé) + +La manière la plus sûre est de définir des mots de passe forts **avant** l'installation via l'interface Runtipi: + +1. Dans Runtipi, avant de cliquer sur "Install" +2. Configurer les variables d'environnement: + - `INDEXER_PASSWORD` - Mot de passe admin de l'indexer (au lieu de "admin") + - `DASHBOARD_PASSWORD` - Mot de passe dashboard→indexer (au lieu de "kibanaserver") + - `API_PASSWORD` - Mot de passe API REST (au lieu de "MyS3cr37P450r.*-") +3. Utiliser des mots de passe forts (minimum 12 caractères, majuscules, minuscules, chiffres, symboles) +4. Procéder à l'installation + +#### Méthode 2: Après l'Installation + +Si vous avez déjà installé Wazuh avec les mots de passe par défaut, vous devez les changer: + +**A. Changer le mot de passe admin du dashboard:** + +1. Se connecter au dashboard: `https://VOTRE_IP:5601` +2. Login: `admin` / `admin` +3. Cliquer sur le menu **☰** (hamburger) en haut à gauche +4. Aller dans **Security → Internal users** +5. Cliquer sur l'utilisateur **admin** +6. Cliquer sur **Edit** +7. Entrer un **nouveau mot de passe fort** +8. Cliquer sur **Save** + +**B. Mettre à jour les variables d'environnement dans Runtipi:** + +1. Dans l'interface Runtipi, aller dans l'application Wazuh +2. Cliquer sur "Settings" ou "Configuration" +3. Mettre à jour `INDEXER_PASSWORD` avec le nouveau mot de passe +4. Redémarrer l'application pour appliquer les changements + +**C. Changer les autres utilisateurs internes (optionnel mais recommandé):** + +Dans le dashboard, sous **Security → Internal users**, vous pouvez également modifier: +- `kibanaserver` - Utilisateur technique dashboard→indexer +- `wazuh-wui` - Utilisateur API REST +- `logstash` - Utilisateur Filebeat→Indexer (si utilisé) + +**Note:** Après modification des mots de passe, assurez-vous de mettre à jour les variables d'environnement correspondantes dans Runtipi et redémarrer les conteneurs pour synchroniser les configurations. + +--- + +## 🔍 Validation Post-Installation + +### Diagnostic Automatique + +Un script de diagnostic complet est fourni pour vérifier la santé de votre installation: + +```bash +# Se connecter en SSH au serveur +ssh user@VOTRE_SERVEUR + +# Exécuter le diagnostic +bash /opt/runtipi/app-data/*/wazuh-runtipi/data/debug/wazuh-health-check.sh +``` + +**Ce script vérifie automatiquement:** +- ✅ Santé de tous les services (healthy/unhealthy) +- ✅ Utilisation disque (~7-8GB attendu pour installation fraîche) +- ✅ Présence des 8 fichiers de sécurité OpenSearch +- ✅ Connectivité réseau entre conteneurs +- ✅ Configuration du dashboard et manager +- ✅ Variables d'environnement SSL Filebeat (méthode officielle) +- ✅ Initialisation de la sécurité OpenSearch + +--- + +## 📱 Déployer des Agents Wazuh + +Une fois Wazuh installé, vous devez déployer des agents sur vos serveurs/postes à surveiller. + +### Architecture Agent ↔ Manager + +``` +Serveur/Desktop à surveiller Serveur Wazuh (Runtipi) +┌─────────────────────┐ ┌────────────────────┐ +│ │ │ │ +│ Wazuh Agent ─────┼──────────────►│ Wazuh Manager │ +│ (Service) │ Port 1514 │ (Collecte) │ +│ │ 1515 │ │ +│ - Logs système │ │ - Analyse │ +│ - Fichiers │ │ - Corrélation │ +│ - Processus │ │ - Alertes │ +│ - Réseau │ │ │ +└─────────────────────┘ └────────────────────┘ +``` + +### Agent Linux (Debian/Ubuntu) + +```bash +# 1. Télécharger l'agent +wget https://packages.wazuh.com/4.x/apt/pool/main/w/wazuh-agent/wazuh-agent_4.14.1-1_amd64.deb + +# 2. Installer +sudo dpkg -i wazuh-agent_4.14.1-1_amd64.deb + +# 3. Configurer l'adresse du manager (remplacer VOTRE_IP par l'IP de votre serveur Runtipi) +sudo sed -i "s/
MANAGER_IP<\/address>/
VOTRE_IP<\/address>/" /var/ossec/etc/ossec.conf + +# 4. Démarrer l'agent +sudo systemctl daemon-reload +sudo systemctl enable wazuh-agent +sudo systemctl start wazuh-agent + +# 5. Vérifier le statut +sudo systemctl status wazuh-agent +``` + +### Agent Windows + +1. **Télécharger** l'installeur: https://packages.wazuh.com/4.x/windows/wazuh-agent-4.14.1-1.msi + +2. **Ouvrir PowerShell en tant qu'Administrateur** et exécuter: + +```powershell +# Installer (remplacer VOTRE_IP par l'IP de votre serveur Runtipi) +msiexec /i wazuh-agent-4.14.1-1.msi /q WAZUH_MANAGER="VOTRE_IP" + +# Démarrer le service +NET START WazuhSvc + +# Vérifier le statut +Get-Service WazuhSvc +``` + +### Agent MacOS + +```bash +# 1. Télécharger l'agent +curl -O https://packages.wazuh.com/4.x/macos/wazuh-agent-4.14.1-1.pkg + +# 2. Installer +sudo installer -pkg wazuh-agent-4.14.1-1.pkg -target / + +# 3. Configurer (remplacer VOTRE_IP) +sudo sed -i '' "s/
MANAGER_IP<\/address>/
VOTRE_IP<\/address>/" /Library/Ossec/etc/ossec.conf + +# 4. Démarrer +sudo /Library/Ossec/bin/wazuh-control start +``` + +### Vérifier les Agents dans le Dashboard + +1. **Se connecter au dashboard** Wazuh +2. **Cliquer sur** ☰ → **Agents** +3. **Vérifier** que vos agents apparaissent avec le statut **"Active"** (après 1-2 minutes) + +Chaque agent doit montrer: +- ✅ **Status:** Active (point vert) +- ✅ **IP Address:** L'IP de la machine +- ✅ **Version:** 4.14.1 +- ✅ **Last keep alive:** < 1 minute + +--- + +## 🎯 Utilisation du Dashboard + +### Sections Principales + +1. **Overview / Vue d'ensemble** + - Résumé des alertes de sécurité + - Événements récents + - Top agents + - Statistiques globales + +2. **Agents** + - Liste de tous les agents + - Statut (actif/déconnecté) + - Détails par agent + - Déploiement de nouveaux agents + +3. **Security Events / Événements** + - Alertes de sécurité en temps réel + - Filtrage par sévérité, agent, règle + - Timeline des événements + - Détails complets des alertes + +4. **Compliance / Conformité** + - PCI DSS + - GDPR + - HIPAA + - NIST 800-53 + - CIS benchmarks + +5. **Vulnerability Detection** + - CVE détectées sur vos systèmes + - Score CVSS + - Packages vulnérables + - Recommandations de mise à jour + +6. **File Integrity Monitoring (FIM)** + - Changements de fichiers système + - Modifications non autorisées + - Ajouts/suppressions de fichiers + +### Exemple: Voir les Alertes de Sécurité + +1. **Menu** ☰ → **Security Events** +2. **Filtrer par sévérité:** Sélectionner "High" ou "Critical" +3. **Cliquer sur une alerte** pour voir les détails complets: + - Description de la menace + - Agent source + - Fichiers/processus impliqués + - Actions recommandées + - Contexte MITRE ATT&CK + +--- + +## 🔧 Configuration Avancée + +### Ports Utilisés + +| Port | Protocole | Service | Usage | +|------|-----------|---------|-------| +| **5601** | HTTPS | Dashboard | Interface web (publique) | +| **9200** | HTTPS | Indexer | API OpenSearch (interne) | +| **1514** | TCP | Manager | Communication avec les agents (publique) | +| **1515** | TCP | Manager | Enrollment des agents (publique) | +| **514** | UDP | Manager | Collection Syslog | +| **55000** | HTTPS | Manager | API REST (interne) | + +**Note:** Seul les ports **5601**, **1514**, **1515** (Dashboard et les agents) sont à exposer publiquement. Les autres ports sont utilisés pour la communication interne entre les composants Wazuh et les agents. + +### Limites de Ressources + +Configuration par défaut des conteneurs: + +| Conteneur | RAM Min | RAM Max | CPU | +|-----------|---------|---------|-----| +| **Indexer** | 1GB | 4GB | 2 cores | +| **Manager** | 512MB | 2GB | 1 core | +| **Dashboard** | 512MB | 1GB | 1 core | + +Pour modifier, éditer le fichier `docker-compose.json` section `deploy.resources.limits`. + +### Espace Disque + +Utilisation disque normale: **~7-8GB** pour une installation fraîche + +L'espace augmente avec: +- Nombre d'agents connectés +- Volume d'événements générés +- Période de rétention des logs (7 jours par défaut) + +Pour surveiller: +```bash +# Vérifier la taille totale (incluant data/) +du -sh /opt/runtipi/app-data/*/wazuh-runtipi + +# Vérifier uniquement les données persistantes +du -sh /opt/runtipi/app-data/*/wazuh-runtipi/data +``` + +### Personnalisation du Manager + +Fichier de configuration principal: `/var/ossec/etc/ossec.conf` + +Exemples de personnalisation: +- Règles de détection personnalisées +- Alertes par email +- Intégrations (Slack, PagerDuty, etc.) +- Configuration FIM (File Integrity Monitoring) +- Politique de rétention des logs + +Voir la documentation officielle: https://documentation.wazuh.com/current/user-manual/reference/ossec-conf/ + +--- + +## 🔄 Mises à Jour + +Runtipi gère automatiquement les mises à jour de l'application Wazuh. + +### Processus de Mise à Jour + +1. **Runtipi détecte** une nouvelle version dans son repository +2. **Télécharge** le nouveau `docker-compose.json` +3. **Redémarre** les conteneurs avec la nouvelle configuration +4. **Préserve** vos données et configurations + +**Aucune action manuelle requise!** ✅ + +### Vérification Post-Mise à Jour + +```bash +# Attendre 2-3 minutes après la mise à jour + +# Vérifier que tous les services sont healthy +docker ps -a | grep wazuh + +# Diagnostic complet +bash /opt/runtipi/app-data/*/wazuh-runtipi/data/debug/wazuh-health-check.sh +``` + +--- + +## 🆘 Dépannage + +### Le Dashboard n'est pas Accessible + +**Symptôme:** Impossible d'accéder à `https://VOTRE_IP:5601` + +**Solutions:** +```bash +# 1. Vérifier que le conteneur dashboard est running +docker ps | grep dashboard + +# 2. Vérifier les logs du dashboard +docker logs wazuh-runtipi_*-wazuh-dashboard-1 + +# 3. Vérifier que le port 5601 est bien exposé +docker port wazuh-runtipi_*-wazuh-dashboard-1 + +# 4. Tester depuis le serveur +curl -I http://localhost:5601 || curl -I https://localhost:5601 +``` + +### Un Service est "Unhealthy" + +**Symptôme:** `docker ps` montre un conteneur avec `(unhealthy)` + +**Solutions:** +```bash +# 1. Voir les logs du service problématique +docker logs wazuh-runtipi_*-wazuh-SERVICE-1 + +# Exemples: +docker logs wazuh-runtipi_*-wazuh-indexer-1 +docker logs wazuh-runtipi_*-wazuh-manager-1 +docker logs wazuh-runtipi_*-wazuh-dashboard-1 + +# 2. Redémarrer le service +docker restart wazuh-runtipi_*-wazuh-SERVICE-1 + +# 3. Si le problème persiste, redémarrer toute la stack +docker restart $(docker ps -q --filter "name=wazuh-runtipi") +``` + +### Les Agents n'Apparaissent Pas + +**Symptôme:** Agents installés mais invisibles dans le dashboard + +**Vérifications:** + +1. **Sur la machine agent:** +```bash +# Linux/MacOS +sudo /var/ossec/bin/agent-auth -m VOTRE_IP +sudo systemctl restart wazuh-agent +sudo tail -f /var/ossec/logs/ossec.log + +# Windows (PowerShell Admin) +Restart-Service WazuhSvc +Get-Content "C:\Program Files (x86)\ossec-agent\ossec.log" -Tail 20 -Wait +``` + +2. **Sur le serveur Wazuh:** +```bash +# Vérifier les logs du manager +docker logs wazuh-runtipi_*-wazuh-manager-1 | grep -i "agent" + +# Vérifier que les ports 1514/1515 sont bien ouverts +docker port wazuh-runtipi_*-wazuh-manager-1 +``` + +### Utilisation Disque Anormalement Élevée + +**Symptôme:** Plus de 20GB utilisés + +**Solutions:** +```bash +# 1. Vérifier la taille actuelle +du -sh /opt/runtipi/app-data/*/wazuh-runtipi/data + +# 2. Vérifier la taille des indices OpenSearch +docker exec wazuh-runtipi_*-wazuh-indexer-1 curl -k -u admin:admin https://localhost:9200/_cat/indices?v + +# 3. Réduire la période de rétention (connexion dashboard) +# Settings → Indices → wazuh-alerts-* → Modifier la rétention +``` + +### "Index Pattern Warning" dans le health-check + +**Symptôme:** Message `No template found for [wazuh-alerts-*]` + +**Explication:** C'est **normal** pour une installation fraîche sans agents! + +Les indices `wazuh-alerts-*` sont créés automatiquement quand: +1. Des agents Wazuh sont connectés +2. Ces agents génèrent des événements/alertes +3. Le manager envoie les données à l'indexer + +**Solution:** Déployez votre premier agent Wazuh. L'alerte disparaîtra automatiquement. + + +--- + +## 📚 Documentation et Ressources + +### Documentation Wazuh Officielle + +- **Getting Started:** https://documentation.wazuh.com/current/getting-started/ +- **User Manual:** https://documentation.wazuh.com/current/user-manual/ +- **Installation Guide:** https://documentation.wazuh.com/current/installation-guide/ +- **API Reference:** https://documentation.wazuh.com/current/user-manual/api/ +- **Ruleset:** https://documentation.wazuh.com/current/user-manual/ruleset/ + +### Projets Source + +- **Wazuh (Core):** https://github.com/wazuh/wazuh +- **Wazuh Docker:** https://github.com/wazuh/wazuh-docker +- **Wazuh Kubernetes:** https://github.com/wazuh/wazuh-kubernetes +- **Wazuh Documentation:** https://github.com/wazuh/wazuh-documentation + +### Communauté et Support + +- **Google Group:** https://groups.google.com/g/wazuh +- **Slack Community:** https://wazuh.com/community/join-us-on-slack/ +- **GitHub Issues:** https://github.com/wazuh/wazuh/issues + +### Formations et Certifications + +- **Wazuh Free Training:** https://wazuh.com/platform/siem/ +- **YouTube Channel:** https://www.youtube.com/@wazuh + +--- + +## 📁 Structure du Projet + +``` +wazuh-runtipi/ +├── docker-compose.json ← Configuration Docker Compose +├── config.json ← Configuration Runtipi +│ +├── data/ +│ ├── config/ +│ │ └── certs.yml ← Configuration certificats SSL +│ │ +│ ├── scripts/ ← Scripts d'initialisation +│ │ ├── init-certs.sh │ Génération certificats SSL +│ │ ├── init-indexer-init.sh │ Initialisation sécurité OpenSearch +│ │ ├── init-manager.sh │ Configuration manager + Filebeat +│ │ └── init-dashboard.sh │ Configuration dashboard + Watchdog +│ │ +│ ├── debug/ +│ │ └── wazuh-health-check.sh ← Script de diagnostic complet +│ │ +│ ├── indexer-security/ ← Configuration sécurité OpenSearch +│ │ └── .gitkeep │ (Dossier vide - tous les fichiers +│ │ │ sont copiés depuis le Docker au +│ │ │ premier démarrage) +│ │ +│ │ # Les 8 fichiers suivants sont automatiquement copiés +│ │ # depuis l'image wazuh-indexer lors du premier démarrage: +│ │ # - config.yml +│ │ # - roles.yml +│ │ # - roles_mapping.yml +│ │ # - internal_users.yml +│ │ # - action_groups.yml +│ │ # - tenants.yml +│ │ # - nodes_dn.yml +│ │ # - whitelist.yml +│ +└── metadata/ + ├── description.md ← Documentation complète (ce fichier) + └── logo.jpg ← Logo de l'application +``` + +**Notes importantes:** + +- **Scripts d'initialisation** dans `data/scripts/` sont montés dans les conteneurs et exécutés au démarrage +- **Architecture simple** : Un script init par conteneur (init-certs.sh, init-indexer-init.sh, init-manager.sh, init-dashboard.sh) +- **Configuration persistante** via symlinks vers les dossiers personnalisés +- **Filebeat** : Configuration automatique via variables d'environnement officielles (FILEBEAT_SSL_VERIFICATION_MODE, SSL_CERTIFICATE_AUTHORITIES, SSL_CERTIFICATE, SSL_KEY) +- **Dashboard Watchdog** surveille le démarrage et gère automatiquement les blocages de migration `.kibana_1` +- **Sécurité OpenSearch** : Les 8 fichiers dans `indexer-security/` sont automatiquement copiés au premier démarrage et **préservés** lors des mises à jour + +--- + +## 🔧 Détails Techniques et Bonnes Pratiques + +### Configuration Filebeat SSL (Méthode Officielle) + +Cette implémentation utilise la **méthode officielle Wazuh** pour configurer Filebeat avec SSL, documentée sur [Docker Hub - Wazuh Manager](https://hub.docker.com/r/wazuh/wazuh-manager). + +**Variables d'environnement utilisées:** +```bash +FILEBEAT_SSL_VERIFICATION_MODE=full +SSL_CERTIFICATE_AUTHORITIES=/var/ossec/etc/certs/root-ca.pem +SSL_CERTIFICATE=/var/ossec/etc/certs/server.pem +SSL_KEY=/var/ossec/etc/certs/server-key.pem +``` + +Le script officiel `cont-init.d/1-config-filebeat` de l'image Wazuh détecte automatiquement ces variables et génère la configuration Filebeat correcte. Cette approche est **préférable** à la création manuelle de `filebeat.yml` car : +- ✅ Respecte le workflow d'initialisation Wazuh natif +- ✅ Évite les conflits avec les scripts internes +- ✅ Simplifie la maintenance (moins de code personnalisé) +- ✅ Garantit la compatibilité avec les futures versions + +### Initialisation et Boucles Logiques + +Tous les scripts d'initialisation utilisent des **boucles logiques basées sur des conditions réelles** plutôt que des délais fixes, permettant une adaptation automatique à la vitesse de chaque machine : + +**init-indexer-init.sh** : +```bash +# Attend la disponibilité réelle de l'API indexer +until curl -ks https://wazuh.indexer:9200; do + sleep 5 +done +``` + +**init-manager.sh** : +```bash +# Vérifie existence ET contenu du fichier ossec.conf +while [ $ELAPSED -lt $TIMEOUT ]; do + if [ -f "$OSSEC_DEFAULT" ] && [ -s "$OSSEC_DEFAULT" ]; then + break + fi + sleep 2 +done +``` + +**init-dashboard.sh** : +```bash +# Vérifie l'API dashboard toutes les 10s (au lieu de 60s fixes) +# Détecte le temps réel de blocage migration avant intervention +while [ $ELAPSED -lt $TIMEOUT ]; do + API_STATUS=$(curl -sk https://localhost:5601/api/status) + if [[ "$API_STATUS" == "200" ]]; then + break + fi + sleep 10 +done +``` + +Cette approche garantit : +- 🚀 **Démarrage rapide** sur machines performantes +- ⏱️ **Patience suffisante** sur machines plus lentes +- 📊 **Logs précis** avec temps réels écoulés + +### Gestion de la Migration Dashboard + +Le watchdog du dashboard détecte et corrige automatiquement le problème connu de blocage de migration `.kibana_1` : + +1. **Détection** : Vérifie la présence de l'index `.kibana_1` toutes les 10 secondes +2. **Patience** : Attend 180 secondes (3 minutes) avant d'intervenir +3. **Intervention** : Supprime l'index bloqué si nécessaire +4. **Restart** : Laisse Runtipi redémarrer automatiquement le dashboard + +Cette logique évite les interventions prématurées tout en garantissant un démarrage réussi. + +--- + +## 📄 Licence et Crédits + +### Wazuh + +- **Licence:** GPL v2 +- **Copyright:** Wazuh, Inc. +- **Site Web:** https://wazuh.com/ + +### Ce Projet + +- **Configuration Runtipi:** synode-it +- **Date:** 2025-12-27 +- **Version:** 4.14.1 + +Ce projet est une configuration Docker Compose de Wazuh optimisée pour Runtipi. Il utilise les images Docker officielles de Wazuh et suit leurs bonnes pratiques de déploiement. + +--- + +**🎉 Votre plateforme de sécurité Wazuh est maintenant prête à protéger votre infrastructure!** + +Pour toute question, consultez la documentation officielle Wazuh ou rejoignez la communauté. diff --git a/apps/wazuh-runtipi/metadata/logo.jpg b/apps/wazuh-runtipi/metadata/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d2439033cf80b01ab955b09c28e222683c0f2ea1 GIT binary patch literal 53169 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

g}3a-M}m-yit7ZWr( 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 R1z5T-Kw$)4@xb3E{uihDk`n*` literal 0 HcmV?d00001 diff --git a/apps/whoami/config.json b/apps/whoami/config.json new file mode 100644 index 0000000..c0bee99 --- /dev/null +++ b/apps/whoami/config.json @@ -0,0 +1,24 @@ +{ + "name": "Whoami", + "id": "whoami", + "available": true, + "short_desc": "Tiny Go server that prints os information and HTTP request to output.", + "author": "traefik", + "port": 8382, + "categories": [ + "utilities" + ], + "description": "Tiny Go webserver that prints OS information and HTTP request to output.", + "tipi_version": 2, + "version": "v1.11.0", + "source": "https://github.com/traefik/whoami", + "exposable": true, + "supported_architectures": [ + "arm64", + "amd64" + ], + "created_at": 1745082405284, + "updated_at": 1745674974072, + "dynamic_config": true, + "form_fields": [] +} \ No newline at end of file diff --git a/apps/whoami/docker-compose.json b/apps/whoami/docker-compose.json new file mode 100644 index 0000000..2f6c4a1 --- /dev/null +++ b/apps/whoami/docker-compose.json @@ -0,0 +1,10 @@ +{ + "services": [ + { + "name": "whoami", + "image": "traefik/whoami:v1.11.0", + "isMain": true, + "internalPort": "80" + } + ] +} diff --git a/apps/whoami/metadata/description.md b/apps/whoami/metadata/description.md new file mode 100644 index 0000000..27cd734 --- /dev/null +++ b/apps/whoami/metadata/description.md @@ -0,0 +1,43 @@ +# Whoami + +Tiny Go webserver that prints OS information and HTTP request to output. + +## Usage + +### Paths + +#### `/[?wait=d]` + +Returns the whoami information (request and network information). + +The optional `wait` query parameter can be provided to tell the server to wait before sending the response. +The duration is expected in Go's [`time.Duration`](https://golang.org/pkg/time/#ParseDuration) format (e.g. `/?wait=100ms` to wait 100 milliseconds). + +The optional `env` query parameter can be set to `true` to add the environment variables to the response. + +#### `/api` + +Returns the whoami information (and some extra information) as JSON. + +The optional `env` query parameter can be set to `true` to add the environment variables to the response. + +#### `/bench` + +Always return the same response (`1`). + +#### `/data?size=n[&unit=u]` + +Creates a response with a size `n`. + +The unit of measure, if specified, accepts the following values: `KB`, `MB`, `GB`, `TB` (optional, default: bytes). + +#### `/echo` + +WebSocket echo. + +#### `/health` + +Heath check. + +- `GET`, `HEAD`, ...: returns a response with the status code defined by the `POST` +- `POST`: changes the status code of the `GET` (`HEAD`, ...) response. diff --git a/apps/whoami/metadata/logo.jpg b/apps/whoami/metadata/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24ed99d3f04b8a10fdaf3708a8b0bdf3ee296b58 GIT binary patch literal 18692 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 literal 0 HcmV?d00001 diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..a28598447958537019db5053a477e7276aeca197 GIT binary patch literal 9101 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 literal 0 HcmV?d00001 diff --git a/config.js b/config.js new file mode 100644 index 0000000..9a95d69 --- /dev/null +++ b/config.js @@ -0,0 +1,3 @@ +export default { + allowedCommands: ["bun ./scripts/update-config.ts", "bun install && bun run test"], +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..925fe34 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "example-appstore", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "bun test" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.14.1" + }, + "dependencies": { + "@runtipi/common": "^0.8.0", + "bun": "^1.2.10", + "zod-validation-error": "^3.4.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..936fd91 --- /dev/null +++ b/renovate.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "automerge": false, + "extends": [ + "config:recommended" + ], + "addLabels": [ + "renovate" + ], + "enabledManagers": ["regex"], + "automergeStrategy": "rebase", + "customManagers": [ + { + "customType": "regex", + "fileMatch": [ + "^.*docker-compose\\.json$" + ], + "matchStrings": [ + "\"image\": \"(?.*?):(?.*?)\"," + ], + "datasourceTemplate": "docker" + } + ], + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "major", + "patch", + "pin", + "digest" + ], + "automerge": false + }, + { + "matchDepTypes": [ + "devDependencies" + ], + "automerge": false + }, + { + "matchPackageNames": [ + "mariadb", + "mysql", + "monogdb", + "postgres", + "redis" + ], + "enabled": false + } + ], + "postUpgradeTasks": { + "commands": [ + "bun ./scripts/update-config.ts {{packageFile}} {{newVersion}}", + "bun install && bun run test" + ], + "fileFilters": [ + "**/*" + ], + "executionMode": "update" + } +} diff --git a/scripts/update-config.ts b/scripts/update-config.ts new file mode 100644 index 0000000..42891c7 --- /dev/null +++ b/scripts/update-config.ts @@ -0,0 +1,35 @@ +import path from "node:path"; +import fs from "fs/promises"; + +const packageFile = process.argv[2]; +const newVersion = process.argv[3]; + +type AppConfig = { + tipi_version: string; + version: string; + updated_at: number; +}; + +const updateAppConfig = async (packageFile: string, newVersion: string) => { + try { + const packageRoot = path.dirname(packageFile); + const configPath = path.join(packageRoot, "config.json"); + + const config = await fs.readFile(configPath, "utf-8"); + const configParsed = JSON.parse(config) as AppConfig; + + configParsed.tipi_version = configParsed.tipi_version + 1; + configParsed.version = newVersion; + configParsed.updated_at = new Date().getTime(); + + await fs.writeFile(configPath, JSON.stringify(configParsed, null, 2)); + } catch (e) { + console.error(`Failed to update app config, error: ${e}`); + } +}; + +if (!packageFile || !newVersion) { + console.error("Usage: node update-config.js "); + process.exit(1); +} +updateAppConfig(packageFile, newVersion); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..01ef57a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "target": "es2022", + "allowJs": true, + "resolveJsonModule": true, + "moduleDetection": "force", + "isolatedModules": true, + "verbatimModuleSyntax": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "module": "NodeNext", + "outDir": "dist", + "sourceMap": true, + "lib": [ + "es2022" + ] + }, + "include": [ + "**/*.ts", + ], +}