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 133 additions and 304 deletions

View File

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

View File

@ -12,7 +12,7 @@ members = [
]
[workspace.package]
version = "1.1.9"
version = "1.1.0"
edition = "2021"
authors = ["Echo <echo@moon-dragon.us>"]
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
ENV DEBIAN_FRONTEND=noninteractive
FROM rust:1.82-bookworm AS rust-builder
RUN apt-get update && apt-get install -y \
build-essential \
curl \
pkg-config \
libssl-dev \
libfontconfig1-dev \
&& 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
# Cache dependencies by building a dummy project first
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 \
crates/pm-agent-client/src crates/pm-auth/src crates/pm-ca/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
COPY crates/ crates/
COPY migrations/ migrations/
RUN cargo build --release
# 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
# ---------------------------------------------------------------------------
# Stage 2: Frontend build (Ubuntu 24.04 + Node.js 20)
# Stage 2: Frontend build
# ---------------------------------------------------------------------------
FROM ubuntu:24.04 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/*
FROM node:20-bookworm-slim AS frontend-builder
WORKDIR /usr/src/app/frontend
COPY frontend/package.json frontend/package-lock.json ./
@ -93,7 +64,6 @@ RUN apt-get update && apt-get install -y \
ca-certificates \
libssl3t64 \
libfontconfig1 \
openssl \
postgresql-client-16 \
argon2 \
curl \

View File

@ -49,8 +49,7 @@ health_check_poll_interval_secs = 300
# Maximum concurrent mTLS agent calls (Tokio Semaphore)
max_concurrent_agent_calls = 64
# Worker heartbeat write interval (seconds). Default: 300 = 5 minutes
heartbeat_interval_secs = 300
# Worker heartbeat write interval (seconds)
# 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

View File

@ -101,8 +101,7 @@ pub struct WorkerConfig {
pub health_check_poll_interval_secs: u64,
/// Maximum concurrent agent calls
pub max_concurrent_agent_calls: usize,
/// Worker heartbeat interval in seconds (default: 300 = 5 min)
#[serde(default = "default_heartbeat_interval")]
/// Worker heartbeat interval in seconds
pub heartbeat_interval_secs: u64,
/// WS relay HTTP polling fallback interval in seconds (default: 10)
pub ws_relay_poll_interval_secs: u64,
@ -256,10 +255,6 @@ fn default_health_check_poll_interval() -> u64 {
300
}
fn default_heartbeat_interval() -> u64 {
300
}
fn default_sso_callback_url() -> 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
* Release v1.1.0

2
debian/control vendored
View File

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

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

@ -107,27 +107,7 @@ setup_database() {
# Store password for config generation
echo "${db_password}" > /tmp/.pm-db-password-new
else
info "PostgreSQL user '${DB_USER}' already exists."
# 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
info "PostgreSQL user '${DB_USER}' already exists, skipping creation."
fi
# Create database if not exists
@ -227,11 +207,8 @@ generate_admin_password() {
admin_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#%^&*' | head -c 24)
# 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
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
# Only update if the placeholder hash is still present
@ -269,56 +246,42 @@ generate_admin_password() {
}
# ---------------------------------------------------------------------------
# 7. Write config.toml with DB URL
# ---------------------------------------------------------------------------
# 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)
# 7. Write config.toml with DB URL (only if file doesn't exist)
# ---------------------------------------------------------------------------
write_config() {
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=""
if [[ -f /tmp/.pm-db-password-new ]]; then
db_password=$(cat /tmp/.pm-db-password-new)
fi
if [[ -f "${config_file}" ]]; then
# Check if the config still has the CHANGEME placeholder
if grep -q 'CHANGEME' "${config_file}"; then
if [[ -z "${db_password}" ]]; then
# No password from setup_database() — generate a fresh one
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}"
# If we don't have a password (user already existed), generate a new one
# and update the PostgreSQL user so we can connect
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
# 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}"
chmod 640 "${config_file}"
info "Configuration written to ${config_file}"
}
# ---------------------------------------------------------------------------
# 8. Generate JWT keys (idempotent)
# Only generates if missing; regenerates verify.pem from signing.pem if lost.
# ---------------------------------------------------------------------------
generate_jwt_keys() {
if [[ ! -f "${CONFIG_DIR}/jwt/signing.pem" ]]; then
@ -329,14 +292,8 @@ generate_jwt_keys() {
chmod 600 "${CONFIG_DIR}/jwt/signing.pem"
chmod 644 "${CONFIG_DIR}/jwt/verify.pem"
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
info "JWT keys already exist, skipping."
info "JWT signing key already exists, skipping."
fi
}
@ -349,9 +306,6 @@ enable_and_start_services() {
# Enable the target (which pulls in web + worker)
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
if systemctl is-active --quiet patch-manager.target 2>/dev/null; then
info "Restarting patch-manager services (upgrade)..."

View File

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

View File

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

View File

@ -12,63 +12,31 @@ CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- fuzzy text search on host names
-- Enumerations
-- ============================================================
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
CREATE TYPE user_role AS ENUM ('admin', 'operator');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'auth_provider') THEN
CREATE TYPE auth_provider AS ENUM ('local', 'azure_sso');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'host_health_status') THEN
CREATE TYPE host_health_status AS ENUM ('pending', 'healthy', 'degraded', 'unreachable');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'job_status') THEN
CREATE TYPE job_status AS ENUM ('queued', 'pending', 'running', 'succeeded', 'failed', 'cancelled');
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 $$;
CREATE TYPE user_role AS ENUM ('admin', 'operator');
CREATE TYPE auth_provider AS ENUM ('local', 'azure_sso');
CREATE TYPE host_health_status AS ENUM ('pending', 'healthy', 'degraded', 'unreachable');
CREATE TYPE job_status AS ENUM ('queued', 'pending', 'running', 'succeeded', 'failed', 'cancelled');
CREATE TYPE job_kind AS ENUM ('patch_apply', 'patch_remove', 'reboot', 'rollback');
CREATE TYPE window_recurrence AS ENUM ('once', 'daily', 'weekly', 'monthly');
CREATE TYPE cert_status AS ENUM ('active', 'revoked', 'expired');
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'
);
-- ============================================================
-- 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(),
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
@ -76,13 +44,13 @@ CREATE TABLE IF NOT EXISTS groups (
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
-- ============================================================
CREATE TABLE IF NOT EXISTS users (
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL DEFAULT '',
@ -105,28 +73,28 @@ CREATE TABLE IF NOT EXISTS users (
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS 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 IF NOT EXISTS idx_users_role ON users (role);
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_azure_oid ON users (azure_oid) WHERE azure_oid IS NOT NULL;
CREATE INDEX idx_users_role ON users (role);
-- ============================================================
-- 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,
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
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
-- ============================================================
CREATE TABLE IF NOT EXISTS refresh_tokens (
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Stored as Argon2id hash of the opaque token bytes
@ -141,14 +109,14 @@ CREATE TABLE IF NOT EXISTS refresh_tokens (
ip_address INET
);
CREATE INDEX IF NOT EXISTS 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_user ON refresh_tokens (user_id);
CREATE INDEX idx_refresh_tokens_expires ON refresh_tokens (expires_at) WHERE revoked = FALSE;
-- ============================================================
-- Hosts
-- ============================================================
CREATE TABLE IF NOT EXISTS hosts (
CREATE TABLE hosts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fqdn TEXT 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)
);
CREATE INDEX IF NOT EXISTS 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 IF NOT EXISTS idx_hosts_ip ON hosts (ip_address);
CREATE INDEX idx_hosts_health_status ON hosts (health_status);
CREATE INDEX idx_hosts_fqdn ON hosts USING gin (fqdn gin_trgm_ops);
CREATE INDEX idx_hosts_ip ON hosts (ip_address);
-- ============================================================
-- 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,
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
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)
-- ============================================================
CREATE TABLE IF NOT EXISTS host_health_data (
CREATE TABLE host_health_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
polled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
@ -198,14 +166,14 @@ CREATE TABLE IF NOT EXISTS host_health_data (
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)
-- ============================================================
-- 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(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
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
);
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)
-- ============================================================
-- Maintenance Windows
-- ============================================================
CREATE TABLE IF NOT EXISTS maintenance_windows (
CREATE TABLE maintenance_windows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
label TEXT NOT NULL DEFAULT '',
@ -239,14 +207,14 @@ CREATE TABLE IF NOT EXISTS maintenance_windows (
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS 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_host ON maintenance_windows (host_id);
CREATE INDEX idx_mw_start ON maintenance_windows (start_at) WHERE enabled = TRUE;
-- ============================================================
-- Patch Jobs
-- ============================================================
CREATE TABLE IF NOT EXISTS patch_jobs (
CREATE TABLE patch_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kind job_kind NOT NULL DEFAULT 'patch_apply',
status job_status NOT NULL DEFAULT 'queued',
@ -265,15 +233,15 @@ CREATE TABLE IF NOT EXISTS patch_jobs (
completed_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS 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 IF NOT EXISTS idx_patch_jobs_user ON patch_jobs (created_by_user_id);
CREATE INDEX idx_patch_jobs_status ON patch_jobs (status);
CREATE INDEX idx_patch_jobs_created ON patch_jobs (created_at DESC);
CREATE INDEX idx_patch_jobs_user ON patch_jobs (created_by_user_id);
-- ============================================================
-- 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(),
job_id UUID NOT NULL REFERENCES patch_jobs(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)
);
CREATE INDEX IF NOT EXISTS 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 IF NOT EXISTS idx_pjh_status ON patch_job_hosts (status);
CREATE INDEX idx_pjh_job ON patch_job_hosts (job_id);
CREATE INDEX idx_pjh_host ON patch_job_hosts (host_id);
CREATE INDEX idx_pjh_status ON patch_job_hosts (status);
-- ============================================================
-- Certificates
-- ============================================================
CREATE TABLE IF NOT EXISTS certificates (
CREATE TABLE certificates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- NULL = root CA cert
host_id UUID REFERENCES hosts(id) ON DELETE CASCADE,
@ -311,15 +279,15 @@ CREATE TABLE IF NOT EXISTS certificates (
cert_pem TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_certs_host ON certificates (host_id);
CREATE INDEX IF NOT EXISTS idx_certs_status ON certificates (status);
CREATE INDEX IF NOT EXISTS idx_certs_expires ON certificates (expires_at);
CREATE INDEX idx_certs_host ON certificates (host_id);
CREATE INDEX idx_certs_status ON certificates (status);
CREATE INDEX idx_certs_expires ON certificates (expires_at);
-- ============================================================
-- Audit Log (tamper-evident, hash-chained)
-- ============================================================
CREATE TABLE IF NOT EXISTS audit_log (
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
action audit_action NOT 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 ''
);
CREATE INDEX IF NOT EXISTS 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 IF NOT EXISTS 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_created ON audit_log (created_at DESC);
CREATE INDEX idx_audit_actor ON audit_log (actor_user_id);
CREATE INDEX idx_audit_action ON audit_log (action);
CREATE INDEX idx_audit_target ON audit_log (target_type, target_id);
-- Retained for 6 months (pruned by worker)
-- ============================================================
-- Azure SSO Configuration
-- ============================================================
CREATE TABLE IF NOT EXISTS azure_sso_config (
CREATE TABLE azure_sso_config (
id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row
enabled BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id TEXT NOT NULL DEFAULT '',
@ -361,7 +329,7 @@ CREATE TABLE IF NOT EXISTS azure_sso_config (
-- System Configuration (key/value runtime settings)
-- ============================================================
CREATE TABLE IF NOT EXISTS system_config (
CREATE TABLE system_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
@ -383,14 +351,13 @@ INSERT INTO system_config (key, value, description) VALUES
('smtp_from', '', 'From address for notifications'),
('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'),
('ip_whitelist', '[]', 'JSON array of allowed CIDR/IP strings; empty = allow all')
ON CONFLICT (key) DO NOTHING;
('ip_whitelist', '[]', 'JSON array of allowed CIDR/IP strings; empty = allow all');
-- ============================================================
-- Worker Heartbeat
-- ============================================================
CREATE TABLE IF NOT EXISTS worker_heartbeat (
CREATE TABLE worker_heartbeat (
id INTEGER PRIMARY KEY DEFAULT 1, -- singleton row
last_seen TIMESTAMPTZ NOT NULL DEFAULT NOW(),
worker_version TEXT NOT NULL DEFAULT '',
@ -401,7 +368,7 @@ CREATE TABLE IF NOT EXISTS worker_heartbeat (
-- 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(),
scan_id UUID NOT NULL,
ip_address INET NOT NULL,
@ -414,5 +381,5 @@ CREATE TABLE IF NOT EXISTS discovery_results (
registered BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE INDEX IF NOT EXISTS 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_scan ON discovery_results (scan_id);
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
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
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
@ -30,21 +30,15 @@ BEGIN
END;
$$;
DO $$ BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_trigger WHERE tgname = 'trg_job_enqueued'
) THEN
CREATE TRIGGER trg_job_enqueued
AFTER INSERT ON patch_jobs
FOR EACH ROW
EXECUTE FUNCTION notify_job_enqueued();
END IF;
END $$;
CREATE TRIGGER trg_job_enqueued
AFTER INSERT ON patch_jobs
FOR EACH ROW
EXECUTE FUNCTION notify_job_enqueued();
-- ============================================================
-- 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)
WHERE retry_next_at IS NOT NULL;

View File

@ -1,7 +1,7 @@
-- Migration 007: Health check configuration and results
-- 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(),
host_id UUID NOT NULL REFERENCES hosts(id) ON DELETE CASCADE,
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)
CREATE TABLE IF NOT EXISTS host_health_check_results (
CREATE TABLE host_health_check_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
check_id UUID NOT NULL REFERENCES host_health_checks(id) ON DELETE CASCADE,
healthy BOOLEAN NOT NULL,
@ -39,4 +39,4 @@ CREATE TABLE IF NOT EXISTS host_health_check_results (
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.
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;

View File

@ -1,7 +1,7 @@
-- Migration: 016_enrollment_requests
-- 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(),
machine_id TEXT NOT NULL UNIQUE,
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'
);
CREATE INDEX IF NOT EXISTS 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_token ON enrollment_requests (polling_token);
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; }
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VERSION="1.1.9"
VERSION="1.1.0"
RELEASE="1"
PKG_NAME="linux-patch-manager"
DEB_NAME="${PKG_NAME}_${VERSION}-${RELEASE}_amd64.deb"