Private
Public Access
1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
b4921d51a0 fix(docker): use ubuntu:24.04 runtime instead of debian:bookworm-slim
Some checks failed
CI Pipeline / Rust Format Check (pull_request) Successful in 6s
CI Pipeline / Clippy Lints (pull_request) Successful in 54s
CI Pipeline / Rust Unit Tests (pull_request) Failing after 1m20s
CI Pipeline / Security Audit (pull_request) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (pull_request) Successful in 15s
CI Pipeline / Build .deb & Release (pull_request) Has been skipped
The project targets Ubuntu 24.04, not Debian Bookworm. Ubuntu 24.04
includes PostgreSQL 16 in default repos, eliminating the need for the
PGDG APT repo workaround. Also fixes libssl3 → libssl3t64 package name
for the time64 transition in Ubuntu 24.04.
2026-06-07 17:34:05 -05:00
455013db8e fix(docker): add PostgreSQL APT repo for postgresql-client-16
Some checks failed
CI Pipeline / Rust Format Check (pull_request) Successful in 4s
CI Pipeline / Clippy Lints (pull_request) Successful in 52s
CI Pipeline / Rust Unit Tests (pull_request) Failing after 1m21s
CI Pipeline / Security Audit (pull_request) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (pull_request) Successful in 14s
CI Pipeline / Build .deb & Release (pull_request) Has been skipped
Debian Bookworm default repos only ship PostgreSQL 15. The Docker
runtime stage needs postgresql-client-16 for the entrypoint script,
so add the official PGDG APT repository.

- Add PGDG GPG key and sources.list entry for bookworm-pgdg
- Install ca-certificates and curl first (needed for repo setup)
- Purge gnupg2 after use to keep image lean
- Verify argon2 package name is correct for Bookworm (it is)
2026-06-07 17:21:37 -05:00
16 changed files with 137 additions and 354 deletions

View File

@ -9,7 +9,6 @@ on:
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions: permissions:
contents: write contents: write
@ -20,7 +19,7 @@ jobs:
name: Rust Format name: Rust Format
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
components: rustfmt components: rustfmt
@ -31,7 +30,7 @@ jobs:
name: Clippy name: Clippy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
components: clippy components: clippy
@ -44,7 +43,7 @@ jobs:
name: Rust Tests name: Rust Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- name: Install system dependencies - name: Install system dependencies
@ -55,7 +54,7 @@ jobs:
name: Security Audit name: Security Audit
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
- run: cargo install cargo-audit && cargo audit - run: cargo install cargo-audit && cargo audit
@ -63,11 +62,11 @@ jobs:
name: Secret scanning name: Secret scanning
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Gitleaks - name: Gitleaks
uses: gitleaks/gitleaks-action@v3 uses: gitleaks/gitleaks-action@v2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -75,9 +74,9 @@ jobs:
name: Frontend Lint name: Frontend Lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'
- name: Install & Lint - name: Install & Lint
@ -88,7 +87,7 @@ jobs:
needs: [rust-format, clippy, rust-test, security-audit, frontend-lint] needs: [rust-format, clippy, rust-test, security-audit, frontend-lint]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Free disk space - name: Free disk space
@ -104,7 +103,7 @@ jobs:
- name: Strip binaries - name: Strip binaries
run: strip target/release/pm-web target/release/pm-worker run: strip target/release/pm-web target/release/pm-worker
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v5 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '20'
- name: Build frontend - name: Build frontend
@ -126,7 +125,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
- name: Upload to GitHub Release - name: Upload to GitHub Release
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v3 uses: softprops/action-gh-release@v2
with: with:
body: ${{ steps.release_notes.outputs.notes }} body: ${{ steps.release_notes.outputs.notes }}
files: linux-patch-manager_*.deb files: linux-patch-manager_*.deb
@ -136,18 +135,17 @@ jobs:
needs: [rust-format, clippy, rust-test, security-audit, frontend-lint] needs: [rust-format, clippy, rust-test, security-audit, frontend-lint]
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 60
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v4 uses: docker/setup-qemu-action@v3
- name: Log in to GitHub Container Registry - name: Log in to GitHub Container Registry
uses: docker/login-action@v4 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@ -155,7 +153,7 @@ jobs:
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@v5
with: with:
images: ghcr.io/draco-lunaris/linux-patch-manager images: ghcr.io/draco-lunaris/linux-patch-manager
tags: | tags: |
@ -165,10 +163,10 @@ jobs:
type=sha type=sha
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v7 uses: docker/build-push-action@v6
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@ -12,7 +12,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "1.1.9" version = "1.1.0"
edition = "2021" edition = "2021"
authors = ["Echo <echo@moon-dragon.us>"] authors = ["Echo <echo@moon-dragon.us>"]
license = "MIT" license = "MIT"

View File

@ -6,36 +6,20 @@
# ============================================================================= # =============================================================================
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Stage 1: Rust build (Ubuntu 24.04 + rustup) # Stage 1: Rust build
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
FROM ubuntu:24.04 AS rust-builder FROM rust:1.82-bookworm AS rust-builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
build-essential \
curl \
pkg-config \ pkg-config \
libssl-dev \ libssl-dev \
libfontconfig1-dev \ libfontconfig1-dev \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Install Rust via rustup (stable channel, provides 1.85+)
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
ENV PATH="/root/.cargo/bin:${PATH}"
WORKDIR /usr/src/app WORKDIR /usr/src/app
# Cache dependencies by building a dummy project first # Cache dependencies by building a dummy project first
COPY Cargo.toml Cargo.lock ./ COPY Cargo.toml Cargo.lock ./
COPY crates/pm-web/Cargo.toml crates/pm-web/Cargo.toml
COPY crates/pm-worker/Cargo.toml crates/pm-worker/Cargo.toml
COPY crates/pm-core/Cargo.toml crates/pm-core/Cargo.toml
COPY crates/pm-agent-client/Cargo.toml crates/pm-agent-client/Cargo.toml
COPY crates/pm-auth/Cargo.toml crates/pm-auth/Cargo.toml
COPY crates/pm-ca/Cargo.toml crates/pm-ca/Cargo.toml
COPY crates/pm-reports/Cargo.toml crates/pm-reports/Cargo.toml
COPY crates/migrate-secrets/Cargo.toml crates/migrate-secrets/Cargo.toml
RUN mkdir -p crates/pm-web/src crates/pm-worker/src crates/pm-core/src \ RUN mkdir -p crates/pm-web/src crates/pm-worker/src crates/pm-core/src \
crates/pm-agent-client/src crates/pm-auth/src crates/pm-ca/src \ crates/pm-agent-client/src crates/pm-auth/src crates/pm-ca/src \
crates/pm-reports/src crates/migrate-secrets/src crates/pm-reports/src crates/migrate-secrets/src
@ -51,7 +35,6 @@ RUN cargo build --release 2>/dev/null || true
# Now build the real project # Now build the real project
COPY crates/ crates/ COPY crates/ crates/
COPY migrations/ migrations/
RUN cargo build --release RUN cargo build --release
# Verify binaries exist # Verify binaries exist
@ -61,21 +44,9 @@ RUN ls -la target/release/pm-web target/release/pm-worker
RUN strip target/release/pm-web target/release/pm-worker RUN strip target/release/pm-web target/release/pm-worker
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Stage 2: Frontend build (Ubuntu 24.04 + Node.js 20) # Stage 2: Frontend build
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
FROM ubuntu:24.04 AS frontend-builder FROM node:20-bookworm-slim AS frontend-builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install Node.js 20 via NodeSource
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app/frontend WORKDIR /usr/src/app/frontend
COPY frontend/package.json frontend/package-lock.json ./ COPY frontend/package.json frontend/package-lock.json ./
@ -93,7 +64,6 @@ RUN apt-get update && apt-get install -y \
ca-certificates \ ca-certificates \
libssl3t64 \ libssl3t64 \
libfontconfig1 \ libfontconfig1 \
openssl \
postgresql-client-16 \ postgresql-client-16 \
argon2 \ argon2 \
curl \ curl \

View File

@ -49,8 +49,7 @@ health_check_poll_interval_secs = 300
# Maximum concurrent mTLS agent calls (Tokio Semaphore) # Maximum concurrent mTLS agent calls (Tokio Semaphore)
max_concurrent_agent_calls = 64 max_concurrent_agent_calls = 64
# Worker heartbeat write interval (seconds). Default: 300 = 5 minutes # Worker heartbeat write interval (seconds)
heartbeat_interval_secs = 300
# WS relay HTTP polling fallback interval (seconds). When WebSocket connection to # WS relay HTTP polling fallback interval (seconds). When WebSocket connection to
# an agent fails, the relay falls back to polling the agent's HTTP API at this # an agent fails, the relay falls back to polling the agent's HTTP API at this

View File

@ -101,8 +101,7 @@ pub struct WorkerConfig {
pub health_check_poll_interval_secs: u64, pub health_check_poll_interval_secs: u64,
/// Maximum concurrent agent calls /// Maximum concurrent agent calls
pub max_concurrent_agent_calls: usize, pub max_concurrent_agent_calls: usize,
/// Worker heartbeat interval in seconds (default: 300 = 5 min) /// Worker heartbeat interval in seconds
#[serde(default = "default_heartbeat_interval")]
pub heartbeat_interval_secs: u64, pub heartbeat_interval_secs: u64,
/// WS relay HTTP polling fallback interval in seconds (default: 10) /// WS relay HTTP polling fallback interval in seconds (default: 10)
pub ws_relay_poll_interval_secs: u64, pub ws_relay_poll_interval_secs: u64,
@ -256,10 +255,6 @@ fn default_health_check_poll_interval() -> u64 {
300 300
} }
fn default_heartbeat_interval() -> u64 {
300
}
fn default_sso_callback_url() -> String { fn default_sso_callback_url() -> String {
"http://localhost:5173/auth/sso/callback".to_string() "http://localhost:5173/auth/sso/callback".to_string()
} }

48
debian/changelog vendored
View File

@ -1,51 +1,3 @@
linux-patch-manager (1.1.9-1) unstable; urgency=low
* Release v1.1.9
-- git-echo <git-echo@moon-dragon.us> Tue, 09 Jun 2026 13:05:59 -0500
linux-patch-manager (1.1.8-1) unstable; urgency=low
* Release v1.1.8
-- git-echo <git-echo@moon-dragon.us> Tue, 09 Jun 2026 11:47:58 -0500
linux-patch-manager (1.1.7-1) unstable; urgency=low
* Release v1.1.7
-- git-echo <git-echo@moon-dragon.us> Tue, 09 Jun 2026 09:11:11 -0500
linux-patch-manager (1.1.6-1) unstable; urgency=low
* Release v1.1.6
-- git-echo <git-echo@moon-dragon.us> Tue, 09 Jun 2026 08:10:52 -0500
linux-patch-manager (1.1.5-1) unstable; urgency=low
* Release v1.1.5
-- git-echo <git-echo@moon-dragon.us> Mon, 08 Jun 2026 20:15:50 -0500
linux-patch-manager (1.1.4-1) unstable; urgency=low
* Release v1.1.4
-- git-echo <git-echo@moon-dragon.us> Mon, 08 Jun 2026 17:30:35 -0500
linux-patch-manager (1.1.2-1) unstable; urgency=low
* Release v1.1.2
-- git-echo <git-echo@moon-dragon.us> Sun, 07 Jun 2026 21:19:18 -0500
linux-patch-manager (1.1.1-1) unstable; urgency=low
* Release v1.1.1
-- git-echo <git-echo@moon-dragon.us> Sun, 07 Jun 2026 18:55:59 -0500
linux-patch-manager (1.1.0-1) unstable; urgency=low linux-patch-manager (1.1.0-1) unstable; urgency=low
* Release v1.1.0 * Release v1.1.0

2
debian/control vendored
View File

@ -1,5 +1,5 @@
Package: linux-patch-manager Package: linux-patch-manager
Version: 1.1.9-1 Version: 1.1.0-1
Architecture: amd64 Architecture: amd64
Maintainer: Moon Dragon <echo@moon-dragon.us> Maintainer: Moon Dragon <echo@moon-dragon.us>
Installed-Size: 45000 Installed-Size: 45000

142
debian/postinst vendored Normal file → Executable file
View File

@ -107,27 +107,7 @@ setup_database() {
# Store password for config generation # Store password for config generation
echo "${db_password}" > /tmp/.pm-db-password-new echo "${db_password}" > /tmp/.pm-db-password-new
else else
info "PostgreSQL user '${DB_USER}' already exists." info "PostgreSQL user '${DB_USER}' already exists, skipping creation."
# Recover the DB password: try from existing config, or generate new.
local config_file="${CONFIG_DIR}/config.toml"
local existing_pw=""
if [[ -f "${config_file}" ]]; then
# Extract password from URL: postgres://user:PASSWORD@host/db
# Use @localhost anchor so passwords containing @ are extracted correctly.
existing_pw=$(sed -n 's|^url = "postgres://[^:]*:\(.*\)@localhost.*"|\1|p' "${config_file}" | head -1)
fi
if [[ -n "${existing_pw}" && "${existing_pw}" != "CHANGEME" ]]; then
# Config has a real password — sync it to PostgreSQL so the app can connect.
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${existing_pw}';" 2>/dev/null || true
echo "${existing_pw}" > /tmp/.pm-db-password-new
info "Synced DB password from existing config to PostgreSQL."
else
# No config or CHANGEME — generate a fresh password and update PostgreSQL.
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${db_password}';" 2>/dev/null || true
echo "${db_password}" > /tmp/.pm-db-password-new
info "Generated new DB password for existing user."
fi
fi fi
# Create database if not exists # Create database if not exists
@ -217,52 +197,7 @@ MIGSQL
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 6. Reassign database object ownership to patch_manager # 6. Generate admin password and update database
# ---------------------------------------------------------------------------
# The postinst runs migrations as the postgres superuser, so all tables,
# types, and sequences created by those migrations are owned by postgres.
# The application connects as patch_manager and needs ownership to ALTER
# tables during upgrades (e.g. 'must be owner of table groups').
# This function reassigns ownership of every database object to patch_manager
# so the application can manage its own schema.
# ---------------------------------------------------------------------------
reassign_ownership() {
info "Reassigning database object ownership to ${DB_USER}..."
# REASSIGN OWNED BY covers all tables, enum types, sequences, and views
# owned by postgres in the current database.
psql_run_db -c "REASSIGN OWNED BY postgres TO ${DB_USER};" \
|| warn "REASSIGN OWNED BY encountered warnings (may be harmless on fresh installs)."
# Schemas are NOT covered by REASSIGN OWNED BY — handle explicitly.
psql_run_db -c "ALTER SCHEMA public OWNER TO ${DB_USER};" \
|| warn "Could not alter public schema owner."
# Grant full privileges so patch_manager can manage all objects
psql_run -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" \
|| warn "Could not grant database privileges."
psql_run_db -c "GRANT ALL PRIVILEGES ON SCHEMA public TO ${DB_USER};" \
|| warn "Could not grant schema privileges."
psql_run_db -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${DB_USER};" \
|| warn "Could not grant table privileges."
psql_run_db -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${DB_USER};" \
|| warn "Could not grant sequence privileges."
psql_run_db -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO ${DB_USER};" \
|| warn "Could not grant function privileges."
# Ensure future objects in public schema are also owned by patch_manager
psql_run_db -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${DB_USER};" \
|| warn "Could not set default table privileges."
psql_run_db -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${DB_USER};" \
|| warn "Could not set default sequence privileges."
psql_run_db -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO ${DB_USER};" \
|| warn "Could not set default function privileges."
info "Database object ownership reassigned to ${DB_USER}."
}
# ---------------------------------------------------------------------------
# 8. Generate admin password and update database
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
generate_admin_password() { generate_admin_password() {
info "Generating admin password..." info "Generating admin password..."
@ -272,11 +207,8 @@ generate_admin_password() {
admin_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#%^&*' | head -c 24) admin_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#%^&*' | head -c 24)
# Hash with argon2 (PHC format, compatible with the application) # Hash with argon2 (PHC format, compatible with the application)
# Generate a random 16-character salt (argon2 requires minimum 8 characters)
local admin_salt
admin_salt=$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9' | head -c 16)
local password_hash local password_hash
password_hash=$(echo -n "${admin_password}" | argon2 "${admin_salt}" -id -t 3 -m 16 -p 1 -l 32 -e) password_hash=$(echo -n "${admin_password}" | argon2 salt -id -t 3 -m 65536 -p 1 -l 32 -e)
# Update admin user password in database # Update admin user password in database
# Only update if the placeholder hash is still present # Only update if the placeholder hash is still present
@ -314,56 +246,42 @@ generate_admin_password() {
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 9. Write config.toml with DB URL # 7. Write config.toml with DB URL (only if file doesn't exist)
# ---------------------------------------------------------------------------
# Handles three scenarios:
# 1. No config file → create from example with real DB password
# 2. Config exists with CHANGEME → replace CHANGEME with real DB password
# 3. Config exists with real password → leave it alone (upgrade)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
write_config() { write_config() {
local config_file="${CONFIG_DIR}/config.toml" local config_file="${CONFIG_DIR}/config.toml"
# Resolve the DB password to use: from setup_database() or generate fresh. if [[ -f "${config_file}" ]]; then
info "Config file ${config_file} already exists, not overwriting."
return 0
fi
info "Writing configuration file..."
# Get the DB password — use the one we just generated if we created the user
local db_password="" local db_password=""
if [[ -f /tmp/.pm-db-password-new ]]; then if [[ -f /tmp/.pm-db-password-new ]]; then
db_password=$(cat /tmp/.pm-db-password-new) db_password=$(cat /tmp/.pm-db-password-new)
fi fi
if [[ -f "${config_file}" ]]; then # If we don't have a password (user already existed), generate a new one
# Check if the config still has the CHANGEME placeholder # and update the PostgreSQL user so we can connect
if grep -q 'CHANGEME' "${config_file}"; then if [[ -z "${db_password}" ]]; then
if [[ -z "${db_password}" ]]; then db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
# No password from setup_database() — generate a fresh one psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${db_password}';" 2>/dev/null || true
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${db_password}';" 2>/dev/null || true
fi
info "Replacing CHANGEME placeholder in existing config with real DB password."
sed -i "s|postgres://patch_manager:CHANGEME@localhost/patch_manager|postgres://${DB_USER}:${db_password}@localhost/${DB_NAME}|" "${config_file}"
else
info "Config file ${config_file} already exists with a real password, leaving it unchanged."
return 0
fi
else
# No config file — create from example
if [[ -z "${db_password}" ]]; then
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
psql_run -c "ALTER ROLE ${DB_USER} WITH PASSWORD '${db_password}';" 2>/dev/null || true
fi
info "Writing configuration file..."
cp /usr/share/patch-manager/config.example.toml "${config_file}"
sed -i "s|postgres://patch_manager:CHANGEME@localhost/patch_manager|postgres://${DB_USER}:${db_password}@localhost/${DB_NAME}|" "${config_file}"
fi fi
# Copy example config and set the DB URL
cp /usr/share/patch-manager/config.example.toml "${config_file}"
sed -i "s|postgres://patch_manager:CHANGEME@localhost/patch_manager|postgres://${DB_USER}:${db_password}@localhost/${DB_NAME}|" "${config_file}"
chown patch-manager:patch-manager "${config_file}" chown patch-manager:patch-manager "${config_file}"
chmod 640 "${config_file}" chmod 640 "${config_file}"
info "Configuration written to ${config_file}" info "Configuration written to ${config_file}"
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 10. Generate JWT keys (idempotent) # 8. Generate JWT keys (idempotent)
# Only generates if missing; regenerates verify.pem from signing.pem if lost.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
generate_jwt_keys() { generate_jwt_keys() {
if [[ ! -f "${CONFIG_DIR}/jwt/signing.pem" ]]; then if [[ ! -f "${CONFIG_DIR}/jwt/signing.pem" ]]; then
@ -374,19 +292,13 @@ generate_jwt_keys() {
chmod 600 "${CONFIG_DIR}/jwt/signing.pem" chmod 600 "${CONFIG_DIR}/jwt/signing.pem"
chmod 644 "${CONFIG_DIR}/jwt/verify.pem" chmod 644 "${CONFIG_DIR}/jwt/verify.pem"
info "JWT keys generated." info "JWT keys generated."
elif [[ ! -f "${CONFIG_DIR}/jwt/verify.pem" ]]; then
info "Regenerating missing JWT verification key from existing signing key..."
openssl pkey -in "${CONFIG_DIR}/jwt/signing.pem" -pubout -out "${CONFIG_DIR}/jwt/verify.pem" 2>/dev/null
chown patch-manager:patch-manager "${CONFIG_DIR}/jwt/verify.pem"
chmod 644 "${CONFIG_DIR}/jwt/verify.pem"
info "JWT verification key regenerated."
else else
info "JWT keys already exist, skipping." info "JWT signing key already exists, skipping."
fi fi
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 11. Enable and start services # 9. Enable and start services
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
enable_and_start_services() { enable_and_start_services() {
systemctl daemon-reload systemctl daemon-reload
@ -394,9 +306,6 @@ enable_and_start_services() {
# Enable the target (which pulls in web + worker) # Enable the target (which pulls in web + worker)
systemctl enable patch-manager.target 2>/dev/null || true systemctl enable patch-manager.target 2>/dev/null || true
# Enable individual services so they survive a reboot
systemctl enable patch-manager-web.service patch-manager-worker.service 2>/dev/null || true
# Start or restart services # Start or restart services
if systemctl is-active --quiet patch-manager.target 2>/dev/null; then if systemctl is-active --quiet patch-manager.target 2>/dev/null; then
info "Restarting patch-manager services (upgrade)..." info "Restarting patch-manager services (upgrade)..."
@ -408,7 +317,7 @@ enable_and_start_services() {
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 12. Install backup cron (idempotent) # 10. Install backup cron (idempotent)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
install_backup_cron() { install_backup_cron() {
if ! crontab -l 2>/dev/null | grep -qF "backup.sh"; then if ! crontab -l 2>/dev/null | grep -qF "backup.sh"; then
@ -427,7 +336,6 @@ case "$1" in
wait_for_postgresql wait_for_postgresql
setup_database setup_database
apply_migrations apply_migrations
reassign_ownership
generate_admin_password generate_admin_password
write_config write_config
generate_jwt_keys generate_jwt_keys

View File

@ -8,7 +8,7 @@
services: services:
db: db:
image: postgres:16 image: postgres:16-bookworm
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_USER: patch_manager POSTGRES_USER: patch_manager

View File

@ -1,7 +1,7 @@
{ {
"name": "patch-manager-ui", "name": "patch-manager-ui",
"private": true, "private": true,
"version": "1.1.9", "version": "1.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -12,63 +12,31 @@ CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy text search on host names
-- Enumerations -- Enumerations
-- ============================================================ -- ============================================================
DO $$ BEGIN CREATE TYPE user_role AS ENUM ('admin', 'operator');
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN CREATE TYPE auth_provider AS ENUM ('local', 'azure_sso');
CREATE TYPE user_role AS ENUM ('admin', 'operator'); CREATE TYPE host_health_status AS ENUM ('pending', 'healthy', 'degraded', 'unreachable');
END IF; CREATE TYPE job_status AS ENUM ('queued', 'pending', 'running', 'succeeded', 'failed', 'cancelled');
END $$; CREATE TYPE job_kind AS ENUM ('patch_apply', 'patch_remove', 'reboot', 'rollback');
DO $$ BEGIN CREATE TYPE window_recurrence AS ENUM ('once', 'daily', 'weekly', 'monthly');
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'auth_provider') THEN CREATE TYPE cert_status AS ENUM ('active', 'revoked', 'expired');
CREATE TYPE auth_provider AS ENUM ('local', 'azure_sso'); CREATE TYPE audit_action AS ENUM (
END IF; 'user_login', 'user_logout', 'user_login_failed',
END $$; 'user_created', 'user_deleted', 'user_updated',
DO $$ BEGIN 'host_registered', 'host_removed',
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'host_health_status') THEN 'group_created', 'group_deleted',
CREATE TYPE host_health_status AS ENUM ('pending', 'healthy', 'degraded', 'unreachable'); 'group_membership_changed',
END IF; 'patch_job_created', 'patch_job_cancelled', 'patch_job_rollback',
END $$; 'maintenance_window_created', 'maintenance_window_updated', 'maintenance_window_deleted',
DO $$ BEGIN 'certificate_issued', 'certificate_renewed', 'certificate_revoked', 'certificate_downloaded',
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_status') THEN 'config_changed',
CREATE TYPE job_status AS ENUM ('queued', 'pending', 'running', 'succeeded', 'failed', 'cancelled'); 'discovery_scan_started'
END IF; );
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_kind') THEN
CREATE TYPE job_kind AS ENUM ('patch_apply', 'patch_remove', 'reboot', 'rollback');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'window_recurrence') THEN
CREATE TYPE window_recurrence AS ENUM ('once', 'daily', 'weekly', 'monthly');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'cert_status') THEN
CREATE TYPE cert_status AS ENUM ('active', 'revoked', 'expired');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'audit_action') THEN
CREATE TYPE audit_action AS ENUM (
'user_login', 'user_logout', 'user_login_failed',
'user_created', 'user_deleted', 'user_updated',
'host_registered', 'host_removed',
'group_created', 'group_deleted',
'group_membership_changed',
'patch_job_created', 'patch_job_cancelled', 'patch_job_rollback',
'maintenance_window_created', 'maintenance_window_updated', 'maintenance_window_deleted',
'certificate_issued', 'certificate_renewed', 'certificate_revoked', 'certificate_downloaded',
'config_changed',
'discovery_scan_started'
);
END IF;
END $$;
-- ============================================================ -- ============================================================
-- Groups (defined before users/hosts for FK ordering) -- Groups (defined before users/hosts for FK ordering)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS groups ( CREATE TABLE groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
@ -76,13 +44,13 @@ CREATE TABLE IF NOT EXISTS groups (
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
CREATE INDEX IF NOT EXISTS idx_groups_name ON groups (name); CREATE INDEX idx_groups_name ON groups (name);
-- ============================================================ -- ============================================================
-- Users -- Users
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL DEFAULT '', display_name TEXT NOT NULL DEFAULT '',
@ -105,28 +73,28 @@ CREATE TABLE IF NOT EXISTS users (
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email); CREATE INDEX idx_users_email ON users (email);
CREATE INDEX IF NOT EXISTS idx_users_azure_oid ON users (azure_oid) WHERE azure_oid IS NOT NULL; CREATE INDEX idx_users_azure_oid ON users (azure_oid) WHERE azure_oid IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_users_role ON users (role); CREATE INDEX idx_users_role ON users (role);
-- ============================================================ -- ============================================================
-- User <-> Group membership -- User <-> Group membership
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS user_groups ( CREATE TABLE user_groups (
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, group_id) PRIMARY KEY (user_id, group_id)
); );
CREATE INDEX IF NOT EXISTS idx_user_groups_group ON user_groups (group_id); CREATE INDEX idx_user_groups_group ON user_groups (group_id);
-- ============================================================ -- ============================================================
-- Refresh Tokens -- Refresh Tokens
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS refresh_tokens ( CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Stored as Argon2id hash of the opaque token bytes -- Stored as Argon2id hash of the opaque token bytes
@ -141,14 +109,14 @@ CREATE TABLE IF NOT EXISTS refresh_tokens (
ip_address INET ip_address INET
); );
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON refresh_tokens (user_id); CREATE INDEX idx_refresh_tokens_user ON refresh_tokens (user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires ON refresh_tokens (expires_at) WHERE revoked = FALSE; CREATE INDEX idx_refresh_tokens_expires ON refresh_tokens (expires_at) WHERE revoked = FALSE;
-- ============================================================ -- ============================================================
-- Hosts -- Hosts
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS hosts ( CREATE TABLE hosts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fqdn TEXT NOT NULL, fqdn TEXT NOT NULL,
ip_address INET NOT NULL, ip_address INET NOT NULL,
@ -168,28 +136,28 @@ CREATE TABLE IF NOT EXISTS hosts (
CONSTRAINT hosts_fqdn_ip_unique UNIQUE (fqdn, ip_address) CONSTRAINT hosts_fqdn_ip_unique UNIQUE (fqdn, ip_address)
); );
CREATE INDEX IF NOT EXISTS idx_hosts_health_status ON hosts (health_status); CREATE INDEX idx_hosts_health_status ON hosts (health_status);
CREATE INDEX IF NOT EXISTS idx_hosts_fqdn ON hosts USING gin (fqdn gin_trgm_ops); CREATE INDEX idx_hosts_fqdn ON hosts USING gin (fqdn gin_trgm_ops);
CREATE INDEX IF NOT EXISTS idx_hosts_ip ON hosts (ip_address); CREATE INDEX idx_hosts_ip ON hosts (ip_address);
-- ============================================================ -- ============================================================
-- Host <-> Group membership -- Host <-> Group membership
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS host_groups ( CREATE TABLE host_groups (
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE, group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (host_id, group_id) PRIMARY KEY (host_id, group_id)
); );
CREATE INDEX IF NOT EXISTS idx_host_groups_group ON host_groups (group_id); CREATE INDEX idx_host_groups_group ON host_groups (group_id);
-- ============================================================ -- ============================================================
-- Host Health Data (cached results from 5-min polls) -- Host Health Data (cached results from 5-min polls)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS host_health_data ( CREATE TABLE host_health_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
polled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), polled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@ -198,14 +166,14 @@ CREATE TABLE IF NOT EXISTS host_health_data (
payload JSONB NOT NULL DEFAULT '{}' payload JSONB NOT NULL DEFAULT '{}'
); );
CREATE INDEX IF NOT EXISTS idx_host_health_host ON host_health_data (host_id, polled_at DESC); CREATE INDEX idx_host_health_host ON host_health_data (host_id, polled_at DESC);
-- Retained for 30 days (pruned by worker) -- Retained for 30 days (pruned by worker)
-- ============================================================ -- ============================================================
-- Host Patch Data (cached results from 30-min polls) -- Host Patch Data (cached results from 30-min polls)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS host_patch_data ( CREATE TABLE host_patch_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
polled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), polled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@ -216,14 +184,14 @@ CREATE TABLE IF NOT EXISTS host_patch_data (
cve_count INTEGER NOT NULL DEFAULT 0 cve_count INTEGER NOT NULL DEFAULT 0
); );
CREATE INDEX IF NOT EXISTS idx_host_patch_host ON host_patch_data (host_id, polled_at DESC); CREATE INDEX idx_host_patch_host ON host_patch_data (host_id, polled_at DESC);
-- Retained for 30 days (pruned by worker) -- Retained for 30 days (pruned by worker)
-- ============================================================ -- ============================================================
-- Maintenance Windows -- Maintenance Windows
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS maintenance_windows ( CREATE TABLE maintenance_windows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
label TEXT NOT NULL DEFAULT '', label TEXT NOT NULL DEFAULT '',
@ -239,14 +207,14 @@ CREATE TABLE IF NOT EXISTS maintenance_windows (
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
CREATE INDEX IF NOT EXISTS idx_mw_host ON maintenance_windows (host_id); CREATE INDEX idx_mw_host ON maintenance_windows (host_id);
CREATE INDEX IF NOT EXISTS idx_mw_start ON maintenance_windows (start_at) WHERE enabled = TRUE; CREATE INDEX idx_mw_start ON maintenance_windows (start_at) WHERE enabled = TRUE;
-- ============================================================ -- ============================================================
-- Patch Jobs -- Patch Jobs
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS patch_jobs ( CREATE TABLE patch_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kind job_kind NOT NULL DEFAULT 'patch_apply', kind job_kind NOT NULL DEFAULT 'patch_apply',
status job_status NOT NULL DEFAULT 'queued', status job_status NOT NULL DEFAULT 'queued',
@ -265,15 +233,15 @@ CREATE TABLE IF NOT EXISTS patch_jobs (
completed_at TIMESTAMPTZ completed_at TIMESTAMPTZ
); );
CREATE INDEX IF NOT EXISTS idx_patch_jobs_status ON patch_jobs (status); CREATE INDEX idx_patch_jobs_status ON patch_jobs (status);
CREATE INDEX IF NOT EXISTS idx_patch_jobs_created ON patch_jobs (created_at DESC); CREATE INDEX idx_patch_jobs_created ON patch_jobs (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_patch_jobs_user ON patch_jobs (created_by_user_id); CREATE INDEX idx_patch_jobs_user ON patch_jobs (created_by_user_id);
-- ============================================================ -- ============================================================
-- Patch Job Hosts (per-host status within a batch job) -- Patch Job Hosts (per-host status within a batch job)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS patch_job_hosts ( CREATE TABLE patch_job_hosts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
job_id UUID NOT NULL REFERENCES patch_jobs(id) ON DELETE CASCADE, job_id UUID NOT NULL REFERENCES patch_jobs(id) ON DELETE CASCADE,
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
@ -289,15 +257,15 @@ CREATE TABLE IF NOT EXISTS patch_job_hosts (
UNIQUE (job_id, host_id) UNIQUE (job_id, host_id)
); );
CREATE INDEX IF NOT EXISTS idx_pjh_job ON patch_job_hosts (job_id); CREATE INDEX idx_pjh_job ON patch_job_hosts (job_id);
CREATE INDEX IF NOT EXISTS idx_pjh_host ON patch_job_hosts (host_id); CREATE INDEX idx_pjh_host ON patch_job_hosts (host_id);
CREATE INDEX IF NOT EXISTS idx_pjh_status ON patch_job_hosts (status); CREATE INDEX idx_pjh_status ON patch_job_hosts (status);
-- ============================================================ -- ============================================================
-- Certificates -- Certificates
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS certificates ( CREATE TABLE certificates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- NULL = root CA cert -- NULL = root CA cert
host_id UUID REFERENCES hosts(id) ON DELETE CASCADE, host_id UUID REFERENCES hosts(id) ON DELETE CASCADE,
@ -311,15 +279,15 @@ CREATE TABLE IF NOT EXISTS certificates (
cert_pem TEXT NOT NULL cert_pem TEXT NOT NULL
); );
CREATE INDEX IF NOT EXISTS idx_certs_host ON certificates (host_id); CREATE INDEX idx_certs_host ON certificates (host_id);
CREATE INDEX IF NOT EXISTS idx_certs_status ON certificates (status); CREATE INDEX idx_certs_status ON certificates (status);
CREATE INDEX IF NOT EXISTS idx_certs_expires ON certificates (expires_at); CREATE INDEX idx_certs_expires ON certificates (expires_at);
-- ============================================================ -- ============================================================
-- Audit Log (tamper-evident, hash-chained) -- Audit Log (tamper-evident, hash-chained)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS audit_log ( CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
action audit_action NOT NULL, action audit_action NOT NULL,
actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
@ -334,17 +302,17 @@ CREATE TABLE IF NOT EXISTS audit_log (
row_hash TEXT NOT NULL DEFAULT '' row_hash TEXT NOT NULL DEFAULT ''
); );
CREATE INDEX IF NOT EXISTS idx_audit_created ON audit_log (created_at DESC); CREATE INDEX idx_audit_created ON audit_log (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log (actor_user_id); CREATE INDEX idx_audit_actor ON audit_log (actor_user_id);
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_log (action); CREATE INDEX idx_audit_action ON audit_log (action);
CREATE INDEX IF NOT EXISTS idx_audit_target ON audit_log (target_type, target_id); CREATE INDEX idx_audit_target ON audit_log (target_type, target_id);
-- Retained for 6 months (pruned by worker) -- Retained for 6 months (pruned by worker)
-- ============================================================ -- ============================================================
-- Azure SSO Configuration -- Azure SSO Configuration
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS azure_sso_config ( CREATE TABLE azure_sso_config (
id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row
enabled BOOLEAN NOT NULL DEFAULT FALSE, enabled BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id TEXT NOT NULL DEFAULT '', tenant_id TEXT NOT NULL DEFAULT '',
@ -361,7 +329,7 @@ CREATE TABLE IF NOT EXISTS azure_sso_config (
-- System Configuration (key/value runtime settings) -- System Configuration (key/value runtime settings)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS system_config ( CREATE TABLE system_config (
key TEXT PRIMARY KEY, key TEXT PRIMARY KEY,
value TEXT NOT NULL, value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
@ -383,14 +351,13 @@ INSERT INTO system_config (key, value, description) VALUES
('smtp_from', '', 'From address for notifications'), ('smtp_from', '', 'From address for notifications'),
('smtp_tls_mode', 'starttls', 'SMTP TLS mode: none, starttls, tls'), ('smtp_tls_mode', 'starttls', 'SMTP TLS mode: none, starttls, tls'),
('web_tls_strategy', 'internal_ca', 'Web UI TLS cert strategy: internal_ca or operator_supplied'), ('web_tls_strategy', 'internal_ca', 'Web UI TLS cert strategy: internal_ca or operator_supplied'),
('ip_whitelist', '[]', 'JSON array of allowed CIDR/IP strings; empty = allow all') ('ip_whitelist', '[]', 'JSON array of allowed CIDR/IP strings; empty = allow all');
ON CONFLICT (key) DO NOTHING;
-- ============================================================ -- ============================================================
-- Worker Heartbeat -- Worker Heartbeat
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS worker_heartbeat ( CREATE TABLE worker_heartbeat (
id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
worker_version TEXT NOT NULL DEFAULT '', worker_version TEXT NOT NULL DEFAULT '',
@ -401,7 +368,7 @@ CREATE TABLE IF NOT EXISTS worker_heartbeat (
-- Discovery Results (transient; cleared before each scan) -- Discovery Results (transient; cleared before each scan)
-- ============================================================ -- ============================================================
CREATE TABLE IF NOT EXISTS discovery_results ( CREATE TABLE discovery_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
scan_id UUID NOT NULL, scan_id UUID NOT NULL,
ip_address INET NOT NULL, ip_address INET NOT NULL,
@ -414,5 +381,5 @@ CREATE TABLE IF NOT EXISTS discovery_results (
registered BOOLEAN NOT NULL DEFAULT FALSE registered BOOLEAN NOT NULL DEFAULT FALSE
); );
CREATE INDEX IF NOT EXISTS idx_discovery_scan ON discovery_results (scan_id); CREATE INDEX idx_discovery_scan ON discovery_results (scan_id);
CREATE INDEX IF NOT EXISTS idx_discovery_ip ON discovery_results (ip_address); CREATE INDEX idx_discovery_ip ON discovery_results (ip_address);

View File

@ -8,11 +8,11 @@
-- When the retry engine should next attempt this host; NULL = not scheduled -- When the retry engine should next attempt this host; NULL = not scheduled
ALTER TABLE patch_job_hosts ALTER TABLE patch_job_hosts
ADD COLUMN IF NOT EXISTS retry_next_at TIMESTAMPTZ; ADD COLUMN retry_next_at TIMESTAMPTZ;
-- Last failure reason captured by the worker for display in the UI -- Last failure reason captured by the worker for display in the UI
ALTER TABLE patch_job_hosts ALTER TABLE patch_job_hosts
ADD COLUMN IF NOT EXISTS last_error TEXT; ADD COLUMN last_error TEXT;
-- ============================================================ -- ============================================================
-- pg_notify trigger: fires when an immediate job is inserted -- pg_notify trigger: fires when an immediate job is inserted
@ -30,21 +30,15 @@ BEGIN
END; END;
$$; $$;
DO $$ BEGIN CREATE TRIGGER trg_job_enqueued
IF NOT EXISTS ( AFTER INSERT ON patch_jobs
SELECT 1 FROM pg_trigger WHERE tgname = 'trg_job_enqueued' FOR EACH ROW
) THEN EXECUTE FUNCTION notify_job_enqueued();
CREATE TRIGGER trg_job_enqueued
AFTER INSERT ON patch_jobs
FOR EACH ROW
EXECUTE FUNCTION notify_job_enqueued();
END IF;
END $$;
-- ============================================================ -- ============================================================
-- Index: efficiently find hosts due for retry -- Index: efficiently find hosts due for retry
-- ============================================================ -- ============================================================
CREATE INDEX IF NOT EXISTS idx_pjh_retry CREATE INDEX idx_pjh_retry
ON patch_job_hosts (retry_next_at) ON patch_job_hosts (retry_next_at)
WHERE retry_next_at IS NOT NULL; WHERE retry_next_at IS NOT NULL;

View File

@ -1,7 +1,7 @@
-- Migration 007: Health check configuration and results -- Migration 007: Health check configuration and results
-- Health checks configured per host (1-5 per host) -- Health checks configured per host (1-5 per host)
CREATE TABLE IF NOT EXISTS host_health_checks ( CREATE TABLE host_health_checks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE, host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
@ -27,10 +27,10 @@ CREATE TABLE IF NOT EXISTS host_health_checks (
) )
); );
CREATE INDEX IF NOT EXISTS idx_health_checks_host ON host_health_checks (host_id); CREATE INDEX idx_health_checks_host ON host_health_checks (host_id);
-- Health check poll results (4-day retention, pruned by worker) -- Health check poll results (4-day retention, pruned by worker)
CREATE TABLE IF NOT EXISTS host_health_check_results ( CREATE TABLE host_health_check_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
check_id UUID NOT NULL REFERENCES host_health_checks(id) ON DELETE CASCADE, check_id UUID NOT NULL REFERENCES host_health_checks(id) ON DELETE CASCADE,
healthy BOOLEAN NOT NULL, healthy BOOLEAN NOT NULL,
@ -39,4 +39,4 @@ CREATE TABLE IF NOT EXISTS host_health_check_results (
checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW() checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
CREATE INDEX IF NOT EXISTS idx_health_results_check ON host_health_check_results (check_id, checked_at DESC); CREATE INDEX idx_health_results_check ON host_health_check_results (check_id, checked_at DESC);

View File

@ -4,7 +4,7 @@
-- FK with ON DELETE SET NULL: if target host deleted, revert to default. -- FK with ON DELETE SET NULL: if target host deleted, revert to default.
ALTER TABLE host_health_checks ALTER TABLE host_health_checks
ADD COLUMN IF NOT EXISTS target_host_id UUID REFERENCES hosts(id) ON DELETE SET NULL; ADD COLUMN target_host_id UUID REFERENCES hosts(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS idx_health_checks_target_host ON host_health_checks (target_host_id) CREATE INDEX idx_health_checks_target_host ON host_health_checks (target_host_id)
WHERE target_host_id IS NOT NULL; WHERE target_host_id IS NOT NULL;

View File

@ -1,7 +1,7 @@
-- Migration: 016_enrollment_requests -- Migration: 016_enrollment_requests
-- Description: Create enrollment_requests table for host self-enrollment -- Description: Create enrollment_requests table for host self-enrollment
CREATE TABLE IF NOT EXISTS enrollment_requests ( CREATE TABLE enrollment_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
machine_id TEXT NOT NULL UNIQUE, machine_id TEXT NOT NULL UNIQUE,
fqdn TEXT NOT NULL, fqdn TEXT NOT NULL,
@ -12,5 +12,5 @@ CREATE TABLE IF NOT EXISTS enrollment_requests (
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours' expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours'
); );
CREATE INDEX IF NOT EXISTS idx_enrollment_requests_token ON enrollment_requests (polling_token); CREATE INDEX idx_enrollment_requests_token ON enrollment_requests (polling_token);
CREATE INDEX IF NOT EXISTS idx_enrollment_requests_expires ON enrollment_requests (expires_at); CREATE INDEX idx_enrollment_requests_expires ON enrollment_requests (expires_at);

View File

@ -22,7 +22,7 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERSION="1.1.9" VERSION="1.1.0"
RELEASE="1" RELEASE="1"
PKG_NAME="linux-patch-manager" PKG_NAME="linux-patch-manager"
DEB_NAME="${PKG_NAME}_${VERSION}-${RELEASE}_amd64.deb" DEB_NAME="${PKG_NAME}_${VERSION}-${RELEASE}_amd64.deb"