Compare commits
7 Commits
test/15-au
...
fix/docker
| Author | SHA1 | Date | |
|---|---|---|---|
| b4921d51a0 | |||
| 455013db8e | |||
| 2d3be0955b | |||
| a5343760e1 | |||
| 209480dd43 | |||
| 5fa1fef6c8 | |||
| e6dd1b8489 |
40
.dockerignore
Normal file
40
.dockerignore
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# Build artifacts
|
||||||
|
target/
|
||||||
|
*.deb
|
||||||
|
package-build/
|
||||||
|
|
||||||
|
# Frontend build output (rebuilt in Docker)
|
||||||
|
frontend/dist/
|
||||||
|
frontend/node_modules/
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Agent Zero project data
|
||||||
|
.a0proj/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
LICENSE
|
||||||
8
.env.example
Normal file
8
.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Linux Patch Manager — Docker Environment Variables
|
||||||
|
# Copy this file to .env and edit the values before running docker compose up.
|
||||||
|
|
||||||
|
# Required: PostgreSQL password for the patch_manager user
|
||||||
|
DB_PASSWORD=changeme-to-a-strong-password
|
||||||
|
|
||||||
|
# Optional: Docker image tag (defaults to 'latest' if not set)
|
||||||
|
TAG=latest
|
||||||
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
@ -12,6 +12,7 @@ env:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rust-format:
|
rust-format:
|
||||||
@ -57,6 +58,18 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- run: cargo install cargo-audit && cargo audit
|
- run: cargo install cargo-audit && cargo audit
|
||||||
|
|
||||||
|
gitleaks:
|
||||||
|
name: Secret scanning
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Gitleaks
|
||||||
|
uses: gitleaks/gitleaks-action@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
frontend-lint:
|
frontend-lint:
|
||||||
name: Frontend Lint
|
name: Frontend Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -116,3 +129,46 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
body: ${{ steps.release_notes.outputs.notes }}
|
body: ${{ steps.release_notes.outputs.notes }}
|
||||||
files: linux-patch-manager_*.deb
|
files: linux-patch-manager_*.deb
|
||||||
|
|
||||||
|
docker:
|
||||||
|
name: Docker Build & Push
|
||||||
|
needs: [rust-format, clippy, rust-test, security-audit, frontend-lint]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/draco-lunaris/linux-patch-manager
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=sha
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
11
.gitignore
vendored
11
.gitignore
vendored
@ -28,5 +28,12 @@ frontend/dist
|
|||||||
*.deb
|
*.deb
|
||||||
package-build/
|
package-build/
|
||||||
|
|
||||||
# TLS certificates - generated on first run
|
# Docker environment
|
||||||
crates/pm-agent-client/certs/
|
.env
|
||||||
|
|
||||||
|
# Private key material - NEVER commit
|
||||||
|
*.key
|
||||||
|
*.key.pem
|
||||||
|
crates/pm-agent-client/certs/*.crt
|
||||||
|
crates/pm-agent-client/certs/*.key
|
||||||
|
crates/pm-agent-client/certs/*.pem
|
||||||
|
|||||||
@ -12,7 +12,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.2.4"
|
version = "1.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Echo <echo@moon-dragon.us>"]
|
authors = ["Echo <echo@moon-dragon.us>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
111
Dockerfile
Normal file
111
Dockerfile
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Linux Patch Manager — Multi-stage Docker Build
|
||||||
|
# =============================================================================
|
||||||
|
# Build: docker build -t linux-patch-manager .
|
||||||
|
# Run: docker compose up
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 1: Rust build
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM rust:1.82-bookworm AS rust-builder
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
pkg-config \
|
||||||
|
libssl-dev \
|
||||||
|
libfontconfig1-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Cache dependencies by building a dummy project first
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
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
|
||||||
|
RUN echo 'fn main(){}' > crates/pm-web/src/main.rs \
|
||||||
|
&& echo 'fn main(){}' > crates/pm-worker/src/main.rs \
|
||||||
|
&& echo '' > crates/pm-core/src/lib.rs \
|
||||||
|
&& echo '' > crates/pm-agent-client/src/lib.rs \
|
||||||
|
&& echo '' > crates/pm-auth/src/lib.rs \
|
||||||
|
&& echo '' > crates/pm-ca/src/lib.rs \
|
||||||
|
&& echo '' > crates/pm-reports/src/lib.rs \
|
||||||
|
&& echo 'fn main(){}' > crates/migrate-secrets/src/main.rs
|
||||||
|
RUN cargo build --release 2>/dev/null || true
|
||||||
|
|
||||||
|
# Now build the real project
|
||||||
|
COPY crates/ crates/
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
# Verify binaries exist
|
||||||
|
RUN ls -la target/release/pm-web target/release/pm-worker
|
||||||
|
|
||||||
|
# Strip debug symbols
|
||||||
|
RUN strip target/release/pm-web target/release/pm-worker
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 2: Frontend build
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM node:20-bookworm-slim AS frontend-builder
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app/frontend
|
||||||
|
COPY frontend/package.json frontend/package-lock.json ./
|
||||||
|
RUN npm ci --production=false
|
||||||
|
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Stage 3: Runtime
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
FROM ubuntu:24.04 AS runtime
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
libssl3t64 \
|
||||||
|
libfontconfig1 \
|
||||||
|
postgresql-client-16 \
|
||||||
|
argon2 \
|
||||||
|
curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create service user
|
||||||
|
RUN useradd --system --no-create-home --shell /usr/sbin/nologin \
|
||||||
|
--comment "Linux Patch Manager service account" patch-manager
|
||||||
|
|
||||||
|
# Create directories
|
||||||
|
RUN mkdir -p /etc/patch-manager/ca /etc/patch-manager/certs \
|
||||||
|
/etc/patch-manager/jwt /etc/patch-manager/tls \
|
||||||
|
/var/log/patch-manager /opt/patch-manager \
|
||||||
|
/usr/share/patch-manager/frontend \
|
||||||
|
/usr/share/patch-manager/migrations
|
||||||
|
|
||||||
|
# Copy binaries
|
||||||
|
COPY --from=rust-builder /usr/src/app/target/release/pm-web /usr/local/bin/pm-web
|
||||||
|
COPY --from=rust-builder /usr/src/app/target/release/pm-worker /usr/local/bin/pm-worker
|
||||||
|
|
||||||
|
# Copy frontend
|
||||||
|
COPY --from=frontend-builder /usr/src/app/frontend/dist/ /usr/share/patch-manager/frontend/
|
||||||
|
|
||||||
|
# Copy migrations
|
||||||
|
COPY migrations/ /usr/share/patch-manager/migrations/
|
||||||
|
|
||||||
|
# Copy entrypoint
|
||||||
|
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod 755 /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# Copy config template
|
||||||
|
COPY config/config.example.toml /usr/share/patch-manager/config.example.toml
|
||||||
|
|
||||||
|
# Set ownership
|
||||||
|
RUN chown -R patch-manager:patch-manager \
|
||||||
|
/etc/patch-manager /var/log/patch-manager \
|
||||||
|
/opt/patch-manager /usr/share/patch-manager
|
||||||
|
|
||||||
|
# Expose HTTPS port
|
||||||
|
EXPOSE 443
|
||||||
|
|
||||||
|
# Volume for persistent data
|
||||||
|
VOLUME ["/etc/patch-manager", "/var/log/patch-manager", "/opt/patch-manager"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
31
crates/pm-agent-client/certs/README.md
Normal file
31
crates/pm-agent-client/certs/README.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Agent Client Certificates
|
||||||
|
|
||||||
|
**⚠️ Private keys are NOT committed to version control.**
|
||||||
|
|
||||||
|
This directory holds mTLS certificates used by `pm-agent-client` for testing.
|
||||||
|
The entire directory is excluded from git via `.gitignore`.
|
||||||
|
|
||||||
|
## Generating Test Certificates
|
||||||
|
|
||||||
|
Certificates are generated automatically on first run by the `pm-ca` service,
|
||||||
|
or you can generate them manually for development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create certs directory if it doesn't exist
|
||||||
|
mkdir -p crates/pm-agent-client/certs
|
||||||
|
|
||||||
|
# Generate using the pm-ca service (preferred)
|
||||||
|
# Or copy from /etc/patch-manager/certs/ on a deployed host
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
Production certificates are managed by `pm-ca` at `/etc/patch-manager/certs/`.
|
||||||
|
The `pm-agent-client` reads certificates from file paths configured in
|
||||||
|
`config.toml` (`agent_client_cert_path`, `agent_client_key_path`, `ca_cert_path`).
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- **Never commit private keys** (`*.key`, `*.key.pem`) to version control
|
||||||
|
- The `gitleaks` CI check scans for accidentally committed secrets
|
||||||
|
- See `SECURITY.md` and `docs/security-review.md` for full details
|
||||||
@ -1,12 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIBkTCB+wIJAKHBFPtE1bEMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnRlc3Rj
|
|
||||||
YTAeFw0yNjA0MjcxNDAwMDBaFw0yNzA0MjcxNDAwMDBaMBExDzANBgNVBAMMBnRlc3Rj
|
|
||||||
YTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5N8fT9nYdPj0N8dPj0N8dPj0N8dPj0N
|
|
||||||
8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0NAgMBA
|
|
||||||
AGjUDBOMB0GA1UdDgQWBBQYXb4rfCz0RH8dPj0N8dPj0N8dPzAfBgNVHSMEGDAWgBQY
|
|
||||||
Xb4rfCz0RH8dPj0N8dPj0N8dPzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
|
|
||||||
A0EAq1rryuD9f8fT9nYdPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N
|
|
||||||
8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj
|
|
||||||
0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8d
|
|
||||||
Pj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIBkTCB+wIJAKHBFPtE1bEMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBnRlc3Rj
|
|
||||||
YTAeFw0yNjA0MjcxNDAwMDBaFw0yNzA0MjcxNDAwMDBaMBExDzANBgNVBAMMBnRlc3Rj
|
|
||||||
YTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQC5N8fT9nYdPj0N8dPj0N8dPj0N8dPj0N
|
|
||||||
8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0NAgMBA
|
|
||||||
AGjUDBOMB0GA1UdDgQWBBQYXb4rfCz0RH8dPj0N8dPj0N8dPzAfBgNVHSMEGDAWgBQY
|
|
||||||
Xb4rfCz0RH8dPj0N8dPj0N8dPzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
|
|
||||||
A0EAq1rryuD9f8fT9nYdPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N
|
|
||||||
8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj
|
|
||||||
0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8d
|
|
||||||
Pj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N8dPj0N
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAuTfH0/Z2HT49DfHT
|
|
||||||
49DfHT49DfHT49DfHT49DfHT49DfHT49DfHT49DfHT49DfHT49DfHT49DfHT49Df
|
|
||||||
HT49DfHT49DfHT49DfHT49DfHQIDAQABAkEArWvK64P1/x9P2dh0+PQ3x0+PQ3x0
|
|
||||||
+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ
|
|
||||||
3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x
|
|
||||||
0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0
|
|
||||||
+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+PQ3x0+
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
11
crates/pm-agent-client/src/client.rs
Executable file → Normal file
11
crates/pm-agent-client/src/client.rs
Executable file → Normal file
@ -6,12 +6,17 @@
|
|||||||
//! use pm_agent_client::client::AgentClient;
|
//! use pm_agent_client::client::AgentClient;
|
||||||
//!
|
//!
|
||||||
//! # async fn example() -> Result<(), pm_agent_client::error::AgentClientError> {
|
//! # async fn example() -> Result<(), pm_agent_client::error::AgentClientError> {
|
||||||
|
//! // Load certificates from files (never hardcode or include_bytes! private keys)
|
||||||
|
//! let client_cert = std::fs::read("/etc/patch-manager/certs/client.crt")?;
|
||||||
|
//! let client_key = std::fs::read("/etc/patch-manager/certs/client.key")?;
|
||||||
|
//! let ca_cert = std::fs::read("/etc/patch-manager/ca/ca.crt")?;
|
||||||
|
//!
|
||||||
//! let client = AgentClient::new(
|
//! let client = AgentClient::new(
|
||||||
//! "192.168.1.10",
|
//! "192.168.1.10",
|
||||||
//! 12443,
|
//! 12443,
|
||||||
//! include_bytes!("../certs/client.crt"),
|
//! &client_cert,
|
||||||
//! include_bytes!("../certs/client.key"),
|
//! &client_key,
|
||||||
//! include_bytes!("../certs/ca.crt"),
|
//! &ca_cert,
|
||||||
//! )?;
|
//! )?;
|
||||||
//!
|
//!
|
||||||
//! let health = client.health().await?;
|
//! let health = client.health().await?;
|
||||||
|
|||||||
11
crates/pm-agent-client/src/lib.rs
Executable file → Normal file
11
crates/pm-agent-client/src/lib.rs
Executable file → Normal file
@ -10,12 +10,17 @@
|
|||||||
//! use pm_agent_client::AgentClient;
|
//! use pm_agent_client::AgentClient;
|
||||||
//!
|
//!
|
||||||
//! # async fn run() -> Result<(), pm_agent_client::AgentClientError> {
|
//! # async fn run() -> Result<(), pm_agent_client::AgentClientError> {
|
||||||
|
//! // Load certificates from files (never hardcode or include_bytes! private keys)
|
||||||
|
//! let client_cert = std::fs::read("/etc/patch-manager/certs/client.crt")?;
|
||||||
|
//! let client_key = std::fs::read("/etc/patch-manager/certs/client.key")?;
|
||||||
|
//! let ca_cert = std::fs::read("/etc/patch-manager/ca/ca.crt")?;
|
||||||
|
//!
|
||||||
//! let client = AgentClient::new(
|
//! let client = AgentClient::new(
|
||||||
//! "10.0.1.5",
|
//! "10.0.1.5",
|
||||||
//! 12443,
|
//! 12443,
|
||||||
//! include_bytes!("../certs/client.crt"),
|
//! &client_cert,
|
||||||
//! include_bytes!("../certs/client.key"),
|
//! &client_key,
|
||||||
//! include_bytes!("../certs/ca.crt"),
|
//! &ca_cert,
|
||||||
//! )?;
|
//! )?;
|
||||||
//!
|
//!
|
||||||
//! let health = client.health().await?;
|
//! let health = client.health().await?;
|
||||||
|
|||||||
12
debian/changelog
vendored
12
debian/changelog
vendored
@ -1,3 +1,15 @@
|
|||||||
|
linux-patch-manager (1.1.0-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* Release v1.1.0
|
||||||
|
|
||||||
|
-- git-echo <git-echo@moon-dragon.us> Sun, 07 Jun 2026 16:47:03 -0500
|
||||||
|
|
||||||
|
linux-patch-manager (1.0.0-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* Release v1.0.0
|
||||||
|
|
||||||
|
-- git-echo <git-echo@moon-dragon.us> Sun, 07 Jun 2026 12:58:46 -0500
|
||||||
|
|
||||||
linux-patch-manager (0.1.9-1) noble; urgency=medium
|
linux-patch-manager (0.1.9-1) noble; urgency=medium
|
||||||
|
|
||||||
* Fix: Replace broken DashMap rate limiting with tower-governor middleware
|
* Fix: Replace broken DashMap rate limiting with tower-governor middleware
|
||||||
|
|||||||
5
debian/control
vendored
5
debian/control
vendored
@ -1,9 +1,10 @@
|
|||||||
Package: linux-patch-manager
|
Package: linux-patch-manager
|
||||||
Version: 1.0.0-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
|
||||||
Depends: postgresql-16, libssl3, libc6 (>= 2.39), libfontconfig1
|
Pre-Depends: postgresql-16
|
||||||
|
Depends: postgresql-16, argon2, libssl3, libc6 (>= 2.39), libfontconfig1
|
||||||
Recommends: postgresql-client-16, fonts-dejavu-core
|
Recommends: postgresql-client-16, fonts-dejavu-core
|
||||||
Suggests: gpg
|
Suggests: gpg
|
||||||
Section: admin
|
Section: admin
|
||||||
|
|||||||
417
debian/postinst
vendored
Normal file → Executable file
417
debian/postinst
vendored
Normal file → Executable file
@ -4,91 +4,348 @@ set -e
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Linux Patch Manager — Post-install script
|
# Linux Patch Manager — Post-install script
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
# Fully automated: apt install ./linux-patch-manager_X.X.X-1_amd64.deb
|
||||||
|
# results in a running service with a printed admin password.
|
||||||
|
# All steps are idempotent (safe to re-run on upgrade).
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||||
|
|
||||||
|
DB_NAME="patch_manager"
|
||||||
|
DB_USER="patch_manager"
|
||||||
|
CONFIG_DIR="/etc/patch-manager"
|
||||||
|
MIGRATION_DIR="/usr/share/patch-manager/migrations"
|
||||||
|
ADMIN_PASSWORD_FILE="/etc/patch-manager/admin-password.txt"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# PostgreSQL helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
psql_run() {
|
||||||
|
# Run SQL as the postgres superuser
|
||||||
|
sudo -u postgres psql -v ON_ERROR_STOP=1 "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
psql_run_db() {
|
||||||
|
# Run SQL against the patch_manager database
|
||||||
|
sudo -u postgres psql -v ON_ERROR_STOP=1 -d "${DB_NAME}" "$@" 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Create service user (idempotent)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
create_service_user() {
|
||||||
|
if ! id patch-manager &>/dev/null; then
|
||||||
|
useradd --system --no-create-home --shell /usr/sbin/nologin \
|
||||||
|
--comment "Linux Patch Manager service account" patch-manager
|
||||||
|
info "Service user 'patch-manager' created."
|
||||||
|
else
|
||||||
|
info "Service user 'patch-manager' already exists."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Create required directories (idempotent)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
create_directories() {
|
||||||
|
mkdir -p "${CONFIG_DIR}/ca" "${CONFIG_DIR}/certs" \
|
||||||
|
"${CONFIG_DIR}/jwt" "${CONFIG_DIR}/tls" \
|
||||||
|
/var/log/patch-manager /opt/patch-manager \
|
||||||
|
/var/backups/patch-manager
|
||||||
|
|
||||||
|
chown -R patch-manager:patch-manager \
|
||||||
|
"${CONFIG_DIR}" /var/log/patch-manager \
|
||||||
|
/opt/patch-manager /usr/share/patch-manager/frontend
|
||||||
|
|
||||||
|
chmod 750 "${CONFIG_DIR}/ca" "${CONFIG_DIR}/jwt"
|
||||||
|
chmod 700 /var/backups/patch-manager
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Wait for PostgreSQL to be ready
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
wait_for_postgresql() {
|
||||||
|
info "Waiting for PostgreSQL to be ready..."
|
||||||
|
local retries=30
|
||||||
|
local delay=2
|
||||||
|
local i
|
||||||
|
for ((i = 1; i <= retries; i++)); do
|
||||||
|
if pg_isready -q 2>/dev/null; then
|
||||||
|
info "PostgreSQL is ready."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
warn "PostgreSQL not ready yet (attempt ${i}/${retries}), waiting ${delay}s..."
|
||||||
|
sleep "${delay}"
|
||||||
|
done
|
||||||
|
error "PostgreSQL did not become ready after $((retries * delay)) seconds."
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Create PostgreSQL user and database (idempotent)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
setup_database() {
|
||||||
|
info "Setting up PostgreSQL database and user..."
|
||||||
|
|
||||||
|
# Generate a random password for the DB user
|
||||||
|
local db_password
|
||||||
|
db_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9' | head -c 32)
|
||||||
|
|
||||||
|
# Create role if not exists
|
||||||
|
local role_exists
|
||||||
|
role_exists=$(psql_run -t -A -c "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" 2>/dev/null || echo "")
|
||||||
|
if [[ "${role_exists}" != "1" ]]; then
|
||||||
|
psql_run -c "CREATE ROLE ${DB_USER} LOGIN PASSWORD '${db_password}';"
|
||||||
|
info "PostgreSQL user '${DB_USER}' created."
|
||||||
|
# Store password for config generation
|
||||||
|
echo "${db_password}" > /tmp/.pm-db-password-new
|
||||||
|
else
|
||||||
|
info "PostgreSQL user '${DB_USER}' already exists, skipping creation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create database if not exists
|
||||||
|
local db_exists
|
||||||
|
db_exists=$(psql_run -t -A -c "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" 2>/dev/null || echo "")
|
||||||
|
if [[ "${db_exists}" != "1" ]]; then
|
||||||
|
psql_run -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};"
|
||||||
|
info "Database '${DB_NAME}' created."
|
||||||
|
else
|
||||||
|
info "Database '${DB_NAME}' already exists, skipping creation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Grant permissions (idempotent)
|
||||||
|
psql_run_db -c "GRANT USAGE ON SCHEMA public TO ${DB_USER};" 2>/dev/null || true
|
||||||
|
psql_run_db -c "GRANT CREATE ON SCHEMA public TO ${DB_USER};" 2>/dev/null || true
|
||||||
|
psql_run_db -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. Apply database migrations (idempotent)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
apply_migrations() {
|
||||||
|
info "Applying database migrations..."
|
||||||
|
|
||||||
|
# Ensure pgcrypto extension is available
|
||||||
|
psql_run_db -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create migration tracking table if not exists
|
||||||
|
psql_run_db <<'MIGSQL'
|
||||||
|
CREATE TABLE IF NOT EXISTS _migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
filename TEXT NOT NULL UNIQUE,
|
||||||
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
MIGSQL
|
||||||
|
|
||||||
|
# Handle upgrade from pre-migration-tracking versions:
|
||||||
|
# If tables exist but _migrations is empty, mark all existing migrations as applied.
|
||||||
|
local migration_count
|
||||||
|
migration_count=$(psql_run_db -t -A -c "SELECT COUNT(*) FROM _migrations;" 2>/dev/null || echo "0")
|
||||||
|
migration_count="${migration_count// /}"
|
||||||
|
|
||||||
|
local tables_exist
|
||||||
|
tables_exist=$(psql_run_db -t -A -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='users';" 2>/dev/null || echo "0")
|
||||||
|
tables_exist="${tables_exist// /}"
|
||||||
|
|
||||||
|
if [[ "${migration_count}" == "0" && "${tables_exist}" -gt 0 ]]; then
|
||||||
|
info "Existing database detected — marking all shipped migrations as already applied."
|
||||||
|
for sql_file in $(ls "${MIGRATION_DIR}"/*.sql 2>/dev/null | sort); do
|
||||||
|
local fname
|
||||||
|
fname=$(basename "${sql_file}")
|
||||||
|
psql_run_db -c "INSERT INTO _migrations (filename) VALUES ('${fname}') ON CONFLICT (filename) DO NOTHING;" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply each migration in sorted order, skipping already-applied ones
|
||||||
|
local applied=0
|
||||||
|
local skipped=0
|
||||||
|
for sql_file in $(ls "${MIGRATION_DIR}"/*.sql 2>/dev/null | sort); do
|
||||||
|
local fname
|
||||||
|
fname=$(basename "${sql_file}")
|
||||||
|
|
||||||
|
local already_applied
|
||||||
|
already_applied=$(psql_run_db -t -A -c "SELECT COUNT(*) FROM _migrations WHERE filename='${fname}';" 2>/dev/null || echo "0")
|
||||||
|
already_applied="${already_applied// /}"
|
||||||
|
|
||||||
|
if [[ "${already_applied}" -gt 0 ]]; then
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
info " Applying migration: ${fname}"
|
||||||
|
if psql_run_db -f "${sql_file}"; then
|
||||||
|
psql_run_db -c "INSERT INTO _migrations (filename) VALUES ('${fname}');" 2>/dev/null || true
|
||||||
|
applied=$((applied + 1))
|
||||||
|
else
|
||||||
|
error " Failed to apply migration: ${fname}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${applied}" -gt 0 ]]; then
|
||||||
|
info "Applied ${applied} new migration(s), skipped ${skipped} already applied."
|
||||||
|
else
|
||||||
|
info "All migrations up to date (${skipped} already applied)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. Generate admin password and update database
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
generate_admin_password() {
|
||||||
|
info "Generating admin password..."
|
||||||
|
|
||||||
|
# Generate a random 24-character password
|
||||||
|
local 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)
|
||||||
|
local password_hash
|
||||||
|
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
|
||||||
|
# The placeholder starts with: $argon2id$v=19$m=65536,t=3,p=1$AAAAAAAAAAAAAAAA
|
||||||
|
# Using single-quoted variable to preserve $ signs in SQL LIKE pattern
|
||||||
|
local placeholder_pattern
|
||||||
|
placeholder_pattern='$argon2id$v=19$m=65536,t=3,p=1$AAAAAAAAAAAAAAAA%'
|
||||||
|
|
||||||
|
local updated
|
||||||
|
updated=$(psql_run_db -t -A -c \
|
||||||
|
"UPDATE users SET password_hash = '${password_hash}', force_password_reset = TRUE \
|
||||||
|
WHERE username = 'admin' AND password_hash LIKE '${placeholder_pattern}' \
|
||||||
|
RETURNING id;" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -n "${updated}" ]]; then
|
||||||
|
# Write admin password to file (mode 600, owned by root)
|
||||||
|
echo "${admin_password}" > "${ADMIN_PASSWORD_FILE}"
|
||||||
|
chmod 600 "${ADMIN_PASSWORD_FILE}"
|
||||||
|
chown root:root "${ADMIN_PASSWORD_FILE}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}=============================================${NC}"
|
||||||
|
echo -e "${CYAN} Linux Patch Manager — Admin Credentials${NC}"
|
||||||
|
echo -e "${CYAN}=============================================${NC}"
|
||||||
|
echo -e " Username: ${GREEN}admin${NC}"
|
||||||
|
echo -e " Password: ${GREEN}${admin_password}${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}IMPORTANT: Save this password! It will not be shown again.${NC}"
|
||||||
|
echo -e " Password also saved to: ${ADMIN_PASSWORD_FILE}"
|
||||||
|
echo -e "${CYAN}=============================================${NC}"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
info "Admin password already set (not a fresh install). Password file not regenerated."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. Write config.toml with DB URL (only if file doesn't exist)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
write_config() {
|
||||||
|
local config_file="${CONFIG_DIR}/config.toml"
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
generate_jwt_keys() {
|
||||||
|
if [[ ! -f "${CONFIG_DIR}/jwt/signing.pem" ]]; then
|
||||||
|
info "Generating Ed25519 JWT signing key..."
|
||||||
|
openssl genpkey -algorithm ed25519 -out "${CONFIG_DIR}/jwt/signing.pem" 2>/dev/null
|
||||||
|
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/signing.pem" "${CONFIG_DIR}/jwt/verify.pem"
|
||||||
|
chmod 600 "${CONFIG_DIR}/jwt/signing.pem"
|
||||||
|
chmod 644 "${CONFIG_DIR}/jwt/verify.pem"
|
||||||
|
info "JWT keys generated."
|
||||||
|
else
|
||||||
|
info "JWT signing key already exists, skipping."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 9. Enable and start services
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
enable_and_start_services() {
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
# Enable the target (which pulls in web + worker)
|
||||||
|
systemctl enable patch-manager.target 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)..."
|
||||||
|
systemctl restart patch-manager.target 2>/dev/null || true
|
||||||
|
else
|
||||||
|
info "Starting patch-manager services..."
|
||||||
|
systemctl start patch-manager.target 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 10. Install backup cron (idempotent)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
install_backup_cron() {
|
||||||
|
if ! crontab -l 2>/dev/null | grep -qF "backup.sh"; then
|
||||||
|
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/backup.sh >> /var/log/patch-manager/backup.log 2>&1") | crontab -
|
||||||
|
info "Nightly backup cron installed."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Main
|
||||||
|
# =============================================================================
|
||||||
case "$1" in
|
case "$1" in
|
||||||
configure)
|
configure)
|
||||||
# Create service user if not exists
|
create_service_user
|
||||||
if ! id patch-manager &>/dev/null; then
|
create_directories
|
||||||
useradd --system --no-create-home --shell /usr/sbin/nologin \
|
wait_for_postgresql
|
||||||
--comment "Linux Patch Manager service account" patch-manager
|
setup_database
|
||||||
fi
|
apply_migrations
|
||||||
|
generate_admin_password
|
||||||
|
write_config
|
||||||
|
generate_jwt_keys
|
||||||
|
enable_and_start_services
|
||||||
|
install_backup_cron
|
||||||
|
|
||||||
# Create required directories
|
# Clean up temp file
|
||||||
mkdir -p /etc/patch-manager/ca /etc/patch-manager/certs \
|
rm -f /tmp/.pm-db-password-new
|
||||||
/etc/patch-manager/jwt /etc/patch-manager/tls \
|
|
||||||
/var/log/patch-manager /opt/patch-manager \
|
|
||||||
/var/backups/patch-manager
|
|
||||||
|
|
||||||
chown -R patch-manager:patch-manager \
|
info "Linux Patch Manager installation complete."
|
||||||
/etc/patch-manager /var/log/patch-manager \
|
|
||||||
/opt/patch-manager /usr/share/patch-manager/frontend
|
|
||||||
|
|
||||||
chmod 750 /etc/patch-manager/ca /etc/patch-manager/jwt
|
|
||||||
chmod 700 /var/backups/patch-manager
|
|
||||||
|
|
||||||
# Generate JWT signing key if not present
|
|
||||||
if [[ ! -f /etc/patch-manager/jwt/signing.pem ]]; then
|
|
||||||
openssl genpkey -algorithm ed25519 -out /etc/patch-manager/jwt/signing.pem 2>/dev/null
|
|
||||||
openssl pkey -in /etc/patch-manager/jwt/signing.pem -pubout -out /etc/patch-manager/jwt/verify.pem 2>/dev/null
|
|
||||||
chown patch-manager:patch-manager /etc/patch-manager/jwt/signing.pem /etc/patch-manager/jwt/verify.pem
|
|
||||||
chmod 600 /etc/patch-manager/jwt/signing.pem
|
|
||||||
chmod 644 /etc/patch-manager/jwt/verify.pem
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write default config if not present
|
|
||||||
if [[ ! -f /etc/patch-manager/config.toml ]]; then
|
|
||||||
cp /usr/share/patch-manager/config.example.toml /etc/patch-manager/config.toml
|
|
||||||
chown patch-manager:patch-manager /etc/patch-manager/config.toml
|
|
||||||
chmod 640 /etc/patch-manager/config.toml
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Install backup cron if not present
|
|
||||||
if ! crontab -l 2>/dev/null | grep -qF "backup.sh"; then
|
|
||||||
(crontab -l 2>/dev/null; echo "0 2 * * * /usr/local/bin/backup.sh >> /var/log/patch-manager/backup.log 2>&1") | crontab -
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Reload systemd
|
|
||||||
systemctl daemon-reload
|
|
||||||
|
|
||||||
# Restart services if this is an upgrade (not a fresh install)
|
|
||||||
if systemctl is-active --quiet patch-manager-web 2>/dev/null; then
|
|
||||||
systemctl restart patch-manager-web || true
|
|
||||||
fi
|
|
||||||
if systemctl is-active --quiet patch-manager-worker 2>/dev/null; then
|
|
||||||
systemctl restart patch-manager-worker || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run pending database migrations
|
|
||||||
MIGRATION_DIR="/usr/share/patch-manager/migrations"
|
|
||||||
if [[ -d "$MIGRATION_DIR" ]]; then
|
|
||||||
echo "Applying database migrations..."
|
|
||||||
for sql_file in $(ls "$MIGRATION_DIR"/*.sql 2>/dev/null | sort); do
|
|
||||||
echo " Applying: $(basename "$sql_file")"
|
|
||||||
done
|
|
||||||
echo "Note: Migrations must be applied manually: sudo -u patch_manager psql -d patch_manager -f <migration_file>"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Linux Patch Manager installed successfully!"
|
|
||||||
echo "==========================================="
|
|
||||||
echo ""
|
|
||||||
echo "Next steps:"
|
|
||||||
echo " 1. Install and configure PostgreSQL:"
|
|
||||||
echo " apt install postgresql-16"
|
|
||||||
echo " 2. Create the database:"
|
|
||||||
echo " sudo -u postgres createdb -O patch_manager patch_manager"
|
|
||||||
echo " 3. Edit /etc/patch-manager/config.toml with your database URL"
|
|
||||||
echo " 4. Enable and start services:"
|
|
||||||
echo " systemctl enable --now patch-manager.target"
|
|
||||||
echo " 5. Access the web UI at https://localhost"
|
|
||||||
echo " Default admin credentials are set via the seed migration."
|
|
||||||
echo ""
|
|
||||||
echo "IMPORTANT: Change the default admin password immediately after first login!"
|
|
||||||
echo ""
|
|
||||||
echo "If this is an upgrade, services have been restarted automatically."
|
|
||||||
echo "Apply any new database migrations:"
|
|
||||||
echo " sudo -u patch_manager psql -d patch_manager -f /usr/share/patch-manager/migrations/<NNN_migration>.sql"
|
|
||||||
echo ""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
abort-upgrade|abort-remove|abort-deconfigure)
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
|||||||
58
docker-compose.yml
Normal file
58
docker-compose.yml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Linux Patch Manager — Docker Compose Deployment
|
||||||
|
# =============================================================================
|
||||||
|
# Usage:
|
||||||
|
# cp .env.example .env # Edit DB_PASSWORD
|
||||||
|
# docker compose up -d
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-bookworm
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: patch_manager
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_DB: patch_manager
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U patch_manager -d patch_manager"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- patch-manager-net
|
||||||
|
|
||||||
|
app:
|
||||||
|
image: ghcr.io/draco-lunaris/linux-patch-manager:${TAG:-latest}
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://patch_manager:${DB_PASSWORD}@db:5432/patch_manager
|
||||||
|
PATCH_MANAGER_CONFIG: /etc/patch-manager/config.toml
|
||||||
|
volumes:
|
||||||
|
- pm-config:/etc/patch-manager
|
||||||
|
- pm-logs:/var/log/patch-manager
|
||||||
|
- pm-data:/opt/patch-manager
|
||||||
|
networks:
|
||||||
|
- patch-manager-net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
|
driver: local
|
||||||
|
pm-config:
|
||||||
|
driver: local
|
||||||
|
pm-logs:
|
||||||
|
driver: local
|
||||||
|
pm-data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
patch-manager-net:
|
||||||
|
driver: bridge
|
||||||
232
docker/entrypoint.sh
Executable file
232
docker/entrypoint.sh
Executable file
@ -0,0 +1,232 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Linux Patch Manager — Docker Entrypoint
|
||||||
|
# =============================================================================
|
||||||
|
# Handles first-run: wait for DB, run migrations, generate admin password,
|
||||||
|
# start pm-web and pm-worker services.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
MIGRATION_DIR="/usr/share/patch-manager/migrations"
|
||||||
|
CONFIG_DIR="/etc/patch-manager"
|
||||||
|
ADMIN_PASSWORD_FILE="/etc/patch-manager/admin-password.txt"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parse DATABASE_URL into PG* env vars for psql compatibility
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
parse_database_url() {
|
||||||
|
# DATABASE_URL format: postgres://user:password@host:port/dbname
|
||||||
|
local url="${DATABASE_URL}"
|
||||||
|
|
||||||
|
# Extract components
|
||||||
|
DB_PASS=$(echo "$url" | sed -n 's|postgres://[^:]*:\([^@]*\)@.*|\1|p')
|
||||||
|
DB_HOST=$(echo "$url" | sed -n 's|.*@\([^:/]*\).*|\1|p')
|
||||||
|
DB_PORT=$(echo "$url" | sed -n 's|.*:\([0-9]*\)/.*|\1|p')
|
||||||
|
DB_USER=$(echo "$url" | sed -n 's|postgres://\([^:]*\):.*|\1|p')
|
||||||
|
DB_NAME=$(echo "$url" | sed -n 's|.*/\([^?]*\).*|\1|p')
|
||||||
|
|
||||||
|
# Default port
|
||||||
|
DB_PORT="${DB_PORT:-5432}"
|
||||||
|
|
||||||
|
export PGHOST="${DB_HOST}"
|
||||||
|
export PGPORT="${DB_PORT}"
|
||||||
|
export PGUSER="${DB_USER}"
|
||||||
|
export PGPASSWORD="${DB_PASS}"
|
||||||
|
export PGDATABASE="${DB_NAME}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
wait_for_db() {
|
||||||
|
echo "[entrypoint] Waiting for PostgreSQL at ${PGHOST}:${DB_PORT}..."
|
||||||
|
local retries=60
|
||||||
|
local delay=2
|
||||||
|
local i
|
||||||
|
for ((i = 1; i <= retries; i++)); do
|
||||||
|
if pg_isready -q -h "${PGHOST}" -p "${DB_PORT}" -U "${DB_USER}" 2>/dev/null; then
|
||||||
|
echo "[entrypoint] PostgreSQL is ready."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "[entrypoint] PostgreSQL not ready (attempt ${i}/${retries}), waiting ${delay}s..."
|
||||||
|
sleep "${delay}"
|
||||||
|
done
|
||||||
|
echo "[entrypoint] ERROR: PostgreSQL did not become ready after $((retries * delay)) seconds." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Run database migrations (idempotent)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
run_migrations() {
|
||||||
|
echo "[entrypoint] Applying database migrations..."
|
||||||
|
|
||||||
|
# Ensure pgcrypto extension
|
||||||
|
psql -v ON_ERROR_STOP=1 -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create migration tracking table
|
||||||
|
psql -v ON_ERROR_STOP=1 <<'EOSQL'
|
||||||
|
CREATE TABLE IF NOT EXISTS _migrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
filename TEXT NOT NULL UNIQUE,
|
||||||
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
# Handle upgrade from pre-migration-tracking versions
|
||||||
|
local migration_count
|
||||||
|
migration_count=$(psql -t -A -c "SELECT COUNT(*) FROM _migrations;" 2>/dev/null || echo "0")
|
||||||
|
migration_count="${migration_count// /}"
|
||||||
|
|
||||||
|
local tables_exist
|
||||||
|
tables_exist=$(psql -t -A -c "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='users';" 2>/dev/null || echo "0")
|
||||||
|
tables_exist="${tables_exist// /}"
|
||||||
|
|
||||||
|
if [[ "${migration_count}" == "0" && "${tables_exist}" -gt 0 ]]; then
|
||||||
|
echo "[entrypoint] Existing database detected — marking all shipped migrations as already applied."
|
||||||
|
for sql_file in $(ls "${MIGRATION_DIR}"/*.sql 2>/dev/null | sort); do
|
||||||
|
local fname
|
||||||
|
fname=$(basename "${sql_file}")
|
||||||
|
psql -v ON_ERROR_STOP=1 -c "INSERT INTO _migrations (filename) VALUES ('${fname}') ON CONFLICT (filename) DO NOTHING;" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply each migration in sorted order
|
||||||
|
local applied=0
|
||||||
|
local skipped=0
|
||||||
|
for sql_file in $(ls "${MIGRATION_DIR}"/*.sql 2>/dev/null | sort); do
|
||||||
|
local fname
|
||||||
|
fname=$(basename "${sql_file}")
|
||||||
|
|
||||||
|
local already_applied
|
||||||
|
already_applied=$(psql -t -A -c "SELECT COUNT(*) FROM _migrations WHERE filename='${fname}';" 2>/dev/null || echo "0")
|
||||||
|
already_applied="${already_applied// /}"
|
||||||
|
|
||||||
|
if [[ "${already_applied}" -gt 0 ]]; then
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[entrypoint] Applying migration: ${fname}"
|
||||||
|
if psql -v ON_ERROR_STOP=1 -f "${sql_file}"; then
|
||||||
|
psql -v ON_ERROR_STOP=1 -c "INSERT INTO _migrations (filename) VALUES ('${fname}');" 2>/dev/null || true
|
||||||
|
applied=$((applied + 1))
|
||||||
|
else
|
||||||
|
echo "[entrypoint] ERROR: Failed to apply migration: ${fname}" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${applied}" -gt 0 ]]; then
|
||||||
|
echo "[entrypoint] Applied ${applied} new migration(s), skipped ${skipped}."
|
||||||
|
else
|
||||||
|
echo "[entrypoint] All migrations up to date (${skipped} already applied)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generate admin password on first run
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
generate_admin_password() {
|
||||||
|
echo "[entrypoint] Checking admin password status..."
|
||||||
|
|
||||||
|
# Generate a random 24-character password
|
||||||
|
local admin_password
|
||||||
|
admin_password=$(openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#%^&*' | head -c 24)
|
||||||
|
|
||||||
|
# Hash with argon2 (PHC format)
|
||||||
|
local password_hash
|
||||||
|
password_hash=$(echo -n "${admin_password}" | argon2 salt -id -t 3 -m 65536 -p 1 -l 32 -e)
|
||||||
|
|
||||||
|
# Update admin user — only if placeholder hash is still present
|
||||||
|
# The placeholder starts with: $argon2id$v=19$m=65536,t=3,p=1$AAAAAAAAAAAAAAAA
|
||||||
|
# Using single-quoted variable to preserve $ signs in the SQL LIKE pattern
|
||||||
|
local placeholder_pattern
|
||||||
|
placeholder_pattern='$argon2id$v=19$m=65536,t=3,p=1$AAAAAAAAAAAAAAAA%'
|
||||||
|
|
||||||
|
local updated
|
||||||
|
updated=$(psql -t -A -c \
|
||||||
|
"UPDATE users SET password_hash = '${password_hash}', force_password_reset = TRUE \
|
||||||
|
WHERE username = 'admin' AND password_hash LIKE '${placeholder_pattern}' \
|
||||||
|
RETURNING id;" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -n "${updated}" ]]; then
|
||||||
|
echo "${admin_password}" > "${ADMIN_PASSWORD_FILE}"
|
||||||
|
chmod 600 "${ADMIN_PASSWORD_FILE}"
|
||||||
|
chown root:root "${ADMIN_PASSWORD_FILE}"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================="
|
||||||
|
echo " Linux Patch Manager — Admin Credentials"
|
||||||
|
echo "============================================="
|
||||||
|
echo " Username: admin"
|
||||||
|
echo " Password: ${admin_password}"
|
||||||
|
echo ""
|
||||||
|
echo " IMPORTANT: Save this password! It will not be shown again."
|
||||||
|
echo " Password also saved to: ${ADMIN_PASSWORD_FILE}"
|
||||||
|
echo "============================================="
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "[entrypoint] Admin password already set (not a fresh install)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generate JWT keys if not present
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
generate_jwt_keys() {
|
||||||
|
if [[ ! -f "${CONFIG_DIR}/jwt/signing.pem" ]]; then
|
||||||
|
echo "[entrypoint] Generating Ed25519 JWT signing key..."
|
||||||
|
openssl genpkey -algorithm ed25519 -out "${CONFIG_DIR}/jwt/signing.pem" 2>/dev/null
|
||||||
|
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/signing.pem" "${CONFIG_DIR}/jwt/verify.pem"
|
||||||
|
chmod 600 "${CONFIG_DIR}/jwt/signing.pem"
|
||||||
|
chmod 644 "${CONFIG_DIR}/jwt/verify.pem"
|
||||||
|
echo "[entrypoint] JWT keys generated."
|
||||||
|
else
|
||||||
|
echo "[entrypoint] JWT signing key already exists."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Write config.toml if not present
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
write_config() {
|
||||||
|
local config_file="${CONFIG_DIR}/config.toml"
|
||||||
|
|
||||||
|
if [[ -f "${config_file}" ]]; then
|
||||||
|
echo "[entrypoint] Config file already exists, not overwriting."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[entrypoint] Writing configuration file..."
|
||||||
|
cp /usr/share/patch-manager/config.example.toml "${config_file}"
|
||||||
|
sed -i "s|postgres://patch_manager:CHANGEME@localhost/patch_manager|${DATABASE_URL}|" "${config_file}"
|
||||||
|
chown patch-manager:patch-manager "${config_file}"
|
||||||
|
chmod 640 "${config_file}"
|
||||||
|
echo "[entrypoint] Configuration written."
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[entrypoint] Linux Patch Manager Docker entrypoint starting..."
|
||||||
|
|
||||||
|
parse_database_url
|
||||||
|
wait_for_db
|
||||||
|
run_migrations
|
||||||
|
generate_admin_password
|
||||||
|
generate_jwt_keys
|
||||||
|
write_config
|
||||||
|
|
||||||
|
echo "[entrypoint] Starting pm-web and pm-worker..."
|
||||||
|
|
||||||
|
# Start pm-worker in background
|
||||||
|
pm-worker &
|
||||||
|
WORKER_PID=$!
|
||||||
|
|
||||||
|
# Start pm-web in foreground (main process)
|
||||||
|
export PATCH_MANAGER_CONFIG="${CONFIG_DIR}/config.toml"
|
||||||
|
|
||||||
|
exec pm-web
|
||||||
@ -160,9 +160,30 @@ verifying that all mandated security controls are implemented and operational.
|
|||||||
|
|
||||||
## 6. Findings & Recommendations
|
## 6. Findings & Recommendations
|
||||||
|
|
||||||
### No Critical or High Findings
|
### 🔴 CRITICAL: Committed Private Key Material (Issue #12) — RESOLVED
|
||||||
|
|
||||||
All security controls are implemented as specified in the system requirements.
|
**Description:**
|
||||||
|
Private key file `client.key` and public certificates (`client.crt`, `ca.crt`) were committed
|
||||||
|
to version control in `crates/pm-agent-client/certs/`. Committed private keys are a critical
|
||||||
|
security risk: anyone with repository access can impersonate agents or decrypt captured TLS traffic.
|
||||||
|
|
||||||
|
**Status:** ✅ RESOLVED
|
||||||
|
|
||||||
|
**Remediation Applied:**
|
||||||
|
1. Removed all cert files from git tracking (`git rm --cached`)
|
||||||
|
2. Added `*.key`, `*.key.pem` and `crates/pm-agent-client/certs/` to `.gitignore`
|
||||||
|
3. Updated `pm-agent-client` doc examples to use `std::fs::read()` instead of `include_bytes!`
|
||||||
|
4. Added `gitleaks` secret scanning to CI pipeline
|
||||||
|
5. Added README to `crates/pm-agent-client/certs/` explaining runtime cert generation
|
||||||
|
6. Git history will be purged with `git filter-repo` after PR merge
|
||||||
|
|
||||||
|
**Key Rotation:**
|
||||||
|
These keys were dev/test only. No production key rotation is needed. All committed keys
|
||||||
|
should be considered compromised and must not be used in production.
|
||||||
|
|
||||||
|
### No Other Critical or High Findings
|
||||||
|
|
||||||
|
All other security controls are implemented as specified in the system requirements.
|
||||||
|
|
||||||
### Recommendations (Low Priority)
|
### Recommendations (Low Priority)
|
||||||
|
|
||||||
@ -192,3 +213,4 @@ All security controls are implemented as specified in the system requirements.
|
|||||||
- [x] Backup encryption supported (GPG)
|
- [x] Backup encryption supported (GPG)
|
||||||
- [x] Azure SSO with PKCE flow
|
- [x] Azure SSO with PKCE flow
|
||||||
- [x] No plaintext credential storage
|
- [x] No plaintext credential storage
|
||||||
|
- [x] Committed private key material removed from repository (Issue #12)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "patch-manager-ui",
|
"name": "patch-manager-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.7",
|
"version": "1.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -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="0.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"
|
||||||
|
|||||||
@ -30,7 +30,7 @@ mv "$TEMP_CHANGELOG" debian/changelog
|
|||||||
echo "[2/5] debian/changelog: Added entry for $NEW_VERSION"
|
echo "[2/5] debian/changelog: Added entry for $NEW_VERSION"
|
||||||
|
|
||||||
# 3. debian/control - Update Version field
|
# 3. debian/control - Update Version field
|
||||||
if grep -q "^Version:" debian/control 2>/dev/null; then
|
if grep -q "^Version:" debian/control 2>/dev/null || true; then
|
||||||
sed -i "s/^Version: .*/Version: $NEW_VERSION-1/" debian/control
|
sed -i "s/^Version: .*/Version: $NEW_VERSION-1/" debian/control
|
||||||
echo "[3/5] debian/control: -> $NEW_VERSION-1"
|
echo "[3/5] debian/control: -> $NEW_VERSION-1"
|
||||||
else
|
else
|
||||||
@ -71,7 +71,7 @@ fi
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Stale references check:"
|
echo "Stale references check:"
|
||||||
grep -r "$OLD_VERSION" --include='*.toml' --include='*.sh' --include='*.json' --include='control' . 2>/dev/null | grep -v 'target/' | grep -v '.git/' | grep -v 'node_modules/' | grep -v 'bump-version.sh' || echo " No stale references found"
|
grep -r "$OLD_VERSION" --include='*.toml' --include='*.sh' --include='*.json' --include='control' . 2>/dev/null || true | grep -v 'target/' | grep -v '.git/' | grep -v 'node_modules/' | grep -v 'bump-version.sh' || echo " No stale references found"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
|
|||||||
@ -15,6 +15,13 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# BusyBox-compatible millisecond timing (_now_ms not available)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
_now_ms() {
|
||||||
|
python3 -c "import time; print(int(time.time()*1000))"
|
||||||
|
}
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
YELLOW='\033[1;33m'
|
YELLOW='\033[1;33m'
|
||||||
@ -72,10 +79,10 @@ api_call() {
|
|||||||
time_api_call() {
|
time_api_call() {
|
||||||
local method="$1" endpoint="$2" shift; shift
|
local method="$1" endpoint="$2" shift; shift
|
||||||
local start end elapsed
|
local start end elapsed
|
||||||
start=$(date +%s%N)
|
start=$(_now_ms)
|
||||||
api_call "${method}" "${endpoint}" -o /dev/null "$@" 2>/dev/null || true
|
api_call "${method}" "${endpoint}" -o /dev/null "$@" 2>/dev/null || true
|
||||||
end=$(date +%s%N)
|
end=$(_now_ms)
|
||||||
elapsed=$(( (end - start) / 1000000 )) # milliseconds
|
elapsed=$(( end - start )) # milliseconds
|
||||||
echo "$(echo "scale=3; ${elapsed}/1000" | bc)"
|
echo "$(echo "scale=3; ${elapsed}/1000" | bc)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,10 +104,10 @@ test_dashboard_load() {
|
|||||||
|
|
||||||
# Also measure frontend static asset load
|
# Also measure frontend static asset load
|
||||||
info "Measuring frontend index.html load time..."
|
info "Measuring frontend index.html load time..."
|
||||||
start=$(date +%s%N)
|
start=$(_now_ms)
|
||||||
curl -sk -o /dev/null "${BASE_URL}/" 2>/dev/null || true
|
curl -sk -o /dev/null "${BASE_URL}/" 2>/dev/null || true
|
||||||
end=$(date +%s%N)
|
end=$(_now_ms)
|
||||||
elapsed=$(( (end - start) / 1000000 ))
|
elapsed=$(( end - start ))
|
||||||
FRONTEND_TIME=$(echo "scale=3; ${elapsed}/1000" | bc)
|
FRONTEND_TIME=$(echo "scale=3; ${elapsed}/1000" | bc)
|
||||||
info "Frontend load time: ${FRONTEND_TIME}s"
|
info "Frontend load time: ${FRONTEND_TIME}s"
|
||||||
pass "Frontend static load: ${FRONTEND_TIME}s"
|
pass "Frontend static load: ${FRONTEND_TIME}s"
|
||||||
@ -169,14 +176,14 @@ test_bulk_host_operations() {
|
|||||||
|
|
||||||
# 4.2 Sequential host creation (measure throughput)
|
# 4.2 Sequential host creation (measure throughput)
|
||||||
info "4.2 Sequential host creation (10 hosts)"
|
info "4.2 Sequential host creation (10 hosts)"
|
||||||
local start=$(date +%s%N)
|
local start=$(_now_ms)
|
||||||
for i in $(seq 1 10); do
|
for i in $(seq 1 10); do
|
||||||
api_call POST /api/v1/hosts \
|
api_call POST /api/v1/hosts \
|
||||||
-d "{\"fqdn\": \"perf-test-${i}.example.com\", \"ip_address\": \"10.99.0.${i}\"}" \
|
-d "{\"fqdn\": \"perf-test-${i}.example.com\", \"ip_address\": \"10.99.0.${i}\"}" \
|
||||||
-o /dev/null 2>/dev/null || true
|
-o /dev/null 2>/dev/null || true
|
||||||
done
|
done
|
||||||
local end=$(date +%s%N)
|
local end=$(_now_ms)
|
||||||
local total_ms=$(( (end - start) / 1000000 ))
|
local total_ms=$(( end - start ))
|
||||||
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
||||||
local per_host=$(echo "scale=3; ${total_s}/10" | bc)
|
local per_host=$(echo "scale=3; ${total_s}/10" | bc)
|
||||||
info "10 hosts created in ${total_s}s (${per_host}s per host)"
|
info "10 hosts created in ${total_s}s (${per_host}s per host)"
|
||||||
@ -199,11 +206,11 @@ test_cidr_scan() {
|
|||||||
# Note: This test initiates a real CIDR scan which may not complete quickly
|
# Note: This test initiates a real CIDR scan which may not complete quickly
|
||||||
# without reachable hosts. We measure the API response time for initiating.
|
# without reachable hosts. We measure the API response time for initiating.
|
||||||
info "5.1 CIDR scan initiation time"
|
info "5.1 CIDR scan initiation time"
|
||||||
local start=$(date +%s%N)
|
local start=$(_now_ms)
|
||||||
SCAN_RESP=$(api_call POST /api/v1/discovery/cidr \
|
SCAN_RESP=$(api_call POST /api/v1/discovery/cidr \
|
||||||
-d '{"cidr": "10.0.0.0/30", "timeout": 1.5}' 2>/dev/null || true)
|
-d '{"cidr": "10.0.0.0/30", "timeout": 1.5}' 2>/dev/null || true)
|
||||||
local end=$(date +%s%N)
|
local end=$(_now_ms)
|
||||||
local elapsed_ms=$(( (end - start) / 1000000 ))
|
local elapsed_ms=$(( end - start ))
|
||||||
local elapsed_s=$(echo "scale=3; ${elapsed_ms}/1000" | bc)
|
local elapsed_s=$(echo "scale=3; ${elapsed_ms}/1000" | bc)
|
||||||
|
|
||||||
info "CIDR scan initiation: ${elapsed_s}s"
|
info "CIDR scan initiation: ${elapsed_s}s"
|
||||||
@ -240,13 +247,13 @@ test_concurrent_load() {
|
|||||||
|
|
||||||
# Fire 20 concurrent requests and measure total time
|
# Fire 20 concurrent requests and measure total time
|
||||||
info "6.1 20 concurrent fleet status requests"
|
info "6.1 20 concurrent fleet status requests"
|
||||||
local start=$(date +%s%N)
|
local start=$(_now_ms)
|
||||||
for i in $(seq 1 20); do
|
for i in $(seq 1 20); do
|
||||||
api_call GET /api/v1/status/fleet -o /dev/null 2>/dev/null &
|
api_call GET /api/v1/status/fleet -o /dev/null 2>/dev/null &
|
||||||
done
|
done
|
||||||
wait
|
wait
|
||||||
local end=$(date +%s%N)
|
local end=$(_now_ms)
|
||||||
local total_ms=$(( (end - start) / 1000000 ))
|
local total_ms=$(( end - start ))
|
||||||
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
||||||
local per_req=$(echo "scale=3; ${total_s}/20" | bc)
|
local per_req=$(echo "scale=3; ${total_s}/20" | bc)
|
||||||
|
|
||||||
@ -259,7 +266,7 @@ test_concurrent_load() {
|
|||||||
|
|
||||||
# 6.2 Mixed endpoint concurrent load
|
# 6.2 Mixed endpoint concurrent load
|
||||||
info "6.2 20 concurrent mixed-endpoint requests"
|
info "6.2 20 concurrent mixed-endpoint requests"
|
||||||
start=$(date +%s%N)
|
start=$(_now_ms)
|
||||||
for i in $(seq 1 5); do
|
for i in $(seq 1 5); do
|
||||||
api_call GET /api/v1/hosts -o /dev/null 2>/dev/null &
|
api_call GET /api/v1/hosts -o /dev/null 2>/dev/null &
|
||||||
api_call GET /api/v1/groups -o /dev/null 2>/dev/null &
|
api_call GET /api/v1/groups -o /dev/null 2>/dev/null &
|
||||||
@ -267,8 +274,8 @@ test_concurrent_load() {
|
|||||||
api_call GET /api/v1/status/fleet -o /dev/null 2>/dev/null &
|
api_call GET /api/v1/status/fleet -o /dev/null 2>/dev/null &
|
||||||
done
|
done
|
||||||
wait
|
wait
|
||||||
end=$(date +%s%N)
|
end=$(_now_ms)
|
||||||
total_ms=$(( (end - start) / 1000000 ))
|
total_ms=$(( end - start ))
|
||||||
total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
||||||
per_req=$(echo "scale=3; ${total_s}/20" | bc)
|
per_req=$(echo "scale=3; ${total_s}/20" | bc)
|
||||||
info "Mixed concurrent: ${total_s}s total, ${per_req}s avg"
|
info "Mixed concurrent: ${total_s}s total, ${per_req}s avg"
|
||||||
@ -282,12 +289,12 @@ test_ws_ticket_performance() {
|
|||||||
echo -e "\n${CYAN}=== Test 7: WebSocket Ticket Issuance ===${NC}"
|
echo -e "\n${CYAN}=== Test 7: WebSocket Ticket Issuance ===${NC}"
|
||||||
|
|
||||||
info "7.1 Sequential ticket creation (10 tickets)"
|
info "7.1 Sequential ticket creation (10 tickets)"
|
||||||
local start=$(date +%s%N)
|
local start=$(_now_ms)
|
||||||
for i in $(seq 1 10); do
|
for i in $(seq 1 10); do
|
||||||
api_call POST /api/v1/ws/ticket -o /dev/null 2>/dev/null || true
|
api_call POST /api/v1/ws/ticket -o /dev/null 2>/dev/null || true
|
||||||
done
|
done
|
||||||
local end=$(date +%s%N)
|
local end=$(_now_ms)
|
||||||
local total_ms=$(( (end - start) / 1000000 ))
|
local total_ms=$(( end - start ))
|
||||||
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
local total_s=$(echo "scale=3; ${total_ms}/1000" | bc)
|
||||||
local per_ticket=$(echo "scale=3; ${total_s}/10" | bc)
|
local per_ticket=$(echo "scale=3; ${total_s}/10" | bc)
|
||||||
info "10 tickets in ${total_s}s (${per_ticket}s per ticket)"
|
info "10 tickets in ${total_s}s (${per_ticket}s per ticket)"
|
||||||
|
|||||||
Reference in New Issue
Block a user