diff --git a/agents/patchmon-agent.sh b/agents/patchmon-agent.sh index 6048652..091839a 100755 --- a/agents/patchmon-agent.sh +++ b/agents/patchmon-agent.sh @@ -11,6 +11,11 @@ CONFIG_FILE="/etc/patchmon/agent.conf" CREDENTIALS_FILE="/etc/patchmon/credentials" LOG_FILE="/var/log/patchmon-agent.log" +# This placeholder will be dynamically replaced by the server when serving this +# script based on the "ignore SSL self-signed" setting. If set to -k, curl will +# ignore certificate validation. Otherwise, it will be empty for secure default. +CURL_FLAGS="" + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -144,7 +149,7 @@ EOF test_credentials() { load_credentials - local response=$(curl -ks -X POST \ + local response=$(curl $CURL_FLAGS -X POST \ -H "Content-Type: application/json" \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ @@ -809,7 +814,7 @@ EOF local payload=$(echo "$base_payload $merged_json" | jq -s '.[0] * .[1]') - local response=$(curl -ks -X POST \ + local response=$(curl $CURL_FLAGS -X POST \ -H "Content-Type: application/json" \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ @@ -870,7 +875,7 @@ EOF ping_server() { load_credentials - local response=$(curl -ks -X POST \ + local response=$(curl $CURL_FLAGS -X POST \ -H "Content-Type: application/json" \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ @@ -913,7 +918,7 @@ check_version() { info "Checking for agent updates..." - local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version") + local response=$(curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/version") if [[ $? -eq 0 ]]; then local current_version=$(echo "$response" | grep -o '"currentVersion":"[^"]*' | cut -d'"' -f4) @@ -945,7 +950,7 @@ check_version() { # Check if auto-update is enabled (both globally and for this host) check_auto_update_enabled() { # Get settings from server using API credentials - local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/settings" 2>/dev/null) + local response=$(curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/settings" 2>/dev/null) if [[ $? -ne 0 ]]; then return 1 fi @@ -970,7 +975,7 @@ check_agent_update_needed() { fi # Get server agent info using API credentials - local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp" 2>/dev/null) + local response=$(curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp" 2>/dev/null) if [[ $? -eq 0 ]]; then local server_version=$(echo "$response" | grep -o '"version":"[^"]*' | cut -d'"' -f4) @@ -1007,7 +1012,7 @@ check_agent_update() { fi # Get server agent info using API credentials - local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp") + local response=$(curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/hosts/agent/timestamp") if [[ $? -eq 0 ]]; then local server_version=$(echo "$response" | grep -o '"version":"[^"]*' | cut -d'"' -f4) @@ -1057,7 +1062,7 @@ update_agent() { cp "$0" "$backup_file" # Download new version using API credentials - if curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -o "/tmp/patchmon-agent-new.sh" "$download_url"; then + if curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -o "/tmp/patchmon-agent-new.sh" "$download_url"; then # Verify the downloaded script is valid if bash -n "/tmp/patchmon-agent-new.sh" 2>/dev/null; then # Replace current script @@ -1090,7 +1095,7 @@ update_agent() { update_crontab() { load_credentials info "Updating crontab with current policy..." - local response=$(curl -ks -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") + local response=$(curl $CURL_FLAGS -H "X-API-ID: $API_ID" -H "X-API-KEY: $API_KEY" -X GET "$PATCHMON_SERVER/api/$API_VERSION/settings/update-interval") if [[ $? -eq 0 ]]; then local update_interval=$(echo "$response" | grep -o '"updateInterval":[0-9]*' | cut -d':' -f2) # Fallback if not found diff --git a/agents/patchmon_install.sh b/agents/patchmon_install.sh index d191847..2f95887 100644 --- a/agents/patchmon_install.sh +++ b/agents/patchmon_install.sh @@ -1,10 +1,15 @@ #!/bin/bash # PatchMon Agent Installation Script -# Usage: curl -ks {PATCHMON_URL}/api/v1/hosts/install -H "X-API-ID: {API_ID}" -H "X-API-KEY: {API_KEY}" | bash +# Usage: curl -s {PATCHMON_URL}/api/v1/hosts/install -H "X-API-ID: {API_ID}" -H "X-API-KEY: {API_KEY}" | bash set -e +# This placeholder will be dynamically replaced by the server when serving this +# script based on the "ignore SSL self-signed" setting. If set to -k, curl will +# ignore certificate validation. Otherwise, it will be empty for secure default. +CURL_FLAGS="" + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' @@ -145,7 +150,7 @@ if [[ -f "/usr/local/bin/patchmon-agent.sh" ]]; then info "📋 Moved existing agent to: /usr/local/bin/patchmon-agent.sh.backup.$(date +%Y%m%d_%H%M%S)" fi -curl -ks \ +curl $CURL_FLAGS \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ "$PATCHMON_URL/api/v1/hosts/agent/download" \ @@ -185,7 +190,7 @@ fi # Step 6: Get update interval policy from server and setup crontab info "⏰ Getting update interval policy from server..." -UPDATE_INTERVAL=$(curl -ks \ +UPDATE_INTERVAL=$(curl $CURL_FLAGS \ -H "X-API-ID: $API_ID" \ -H "X-API-KEY: $API_KEY" \ "$PATCHMON_URL/api/v1/settings/update-interval" | \ diff --git a/agents/patchmon_remove.sh b/agents/patchmon_remove.sh index c6328e4..827050b 100755 --- a/agents/patchmon_remove.sh +++ b/agents/patchmon_remove.sh @@ -1,11 +1,16 @@ #!/bin/bash # PatchMon Agent Removal Script -# Usage: curl -ks {PATCHMON_URL}/api/v1/hosts/remove | bash +# Usage: curl -s {PATCHMON_URL}/api/v1/hosts/remove | bash # This script completely removes PatchMon from the system set -e +# This placeholder will be dynamically replaced by the server when serving this +# script based on the "ignore SSL self-signed" setting for any curl calls in +# future (left for consistency with install script). +CURL_FLAGS="" + # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' diff --git a/backend/prisma/migrations/20250925192645_fix_dashboard_preferences_unique_constraint/migration.sql b/backend/prisma/migrations/20250925192645_fix_dashboard_preferences_unique_constraint/migration.sql new file mode 100644 index 0000000..c1f1d33 --- /dev/null +++ b/backend/prisma/migrations/20250925192645_fix_dashboard_preferences_unique_constraint/migration.sql @@ -0,0 +1,10 @@ +-- Fix dashboard preferences unique constraint +-- This migration fixes the unique constraint on dashboard_preferences table + +-- Drop existing indexes if they exist +DROP INDEX IF EXISTS "dashboard_preferences_card_id_key"; +DROP INDEX IF EXISTS "dashboard_preferences_user_id_card_id_key"; +DROP INDEX IF EXISTS "dashboard_preferences_user_id_key"; + +-- Add the correct unique constraint +ALTER TABLE "dashboard_preferences" ADD CONSTRAINT "dashboard_preferences_user_id_card_id_key" UNIQUE ("user_id", "card_id"); diff --git a/backend/prisma/migrations/20250930195941_add_ignore_ssl_self_signed/migration.sql b/backend/prisma/migrations/20250930195941_add_ignore_ssl_self_signed/migration.sql new file mode 100644 index 0000000..f583d23 --- /dev/null +++ b/backend/prisma/migrations/20250930195941_add_ignore_ssl_self_signed/migration.sql @@ -0,0 +1,4 @@ +-- Add ignore_ssl_self_signed column to settings table +-- This allows users to configure whether curl commands should ignore SSL certificate validation + +ALTER TABLE "settings" ADD COLUMN "ignore_ssl_self_signed" BOOLEAN NOT NULL DEFAULT false; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 563c53f..662018e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -7,7 +7,6 @@ datasource db { url = env("DATABASE_URL") } - model dashboard_preferences { id String @id user_id String @@ -157,6 +156,7 @@ model settings { update_available Boolean @default(false) signup_enabled Boolean @default(false) default_user_role String @default("user") + ignore_ssl_self_signed Boolean @default(false) } model update_history { @@ -175,8 +175,6 @@ model users { username String @unique email String @unique password_hash String - first_name String? - last_name String? role String @default("admin") is_active Boolean @default(true) last_login DateTime? @@ -185,5 +183,7 @@ model users { tfa_backup_codes String? tfa_enabled Boolean @default(false) tfa_secret String? + first_name String? + last_name String? dashboard_preferences dashboard_preferences[] } diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js index 8c8dca8..1d561f3 100644 --- a/backend/src/routes/hostRoutes.js +++ b/backend/src/routes/hostRoutes.js @@ -45,11 +45,26 @@ router.get("/agent/download", async (req, res) => { } // Read file and convert line endings - const scriptContent = fs + let scriptContent = fs .readFileSync(agentPath, "utf8") .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); + // Determine curl flags dynamically from settings for consistency + let curlFlags = "-s"; + try { + const settings = await prisma.settings.findFirst(); + if (settings && settings.ignore_ssl_self_signed === true) { + curlFlags = "-sk"; + } + } catch (_) {} + + // Inject the curl flags into the script + scriptContent = scriptContent.replace( + 'CURL_FLAGS=""', + `CURL_FLAGS="${curlFlags}"`, + ); + res.setHeader("Content-Type", "application/x-shellscript"); res.setHeader( "Content-Disposition", @@ -1101,11 +1116,21 @@ router.get("/install", async (req, res) => { ); } - // Inject the API credentials and server URL into the script as environment variables + // Determine curl flags dynamically from settings (ignore self-signed) + let curlFlags = "-s"; + try { + const settings = await prisma.settings.findFirst(); + if (settings && settings.ignore_ssl_self_signed === true) { + curlFlags = "-sk"; + } + } catch (_) {} + + // Inject the API credentials, server URL, and curl flags into the script const envVars = `#!/bin/bash export PATCHMON_URL="${serverUrl}" export API_ID="${host.api_id}" export API_KEY="${host.api_key}" +export CURL_FLAGS="${curlFlags}" `; @@ -1141,7 +1166,24 @@ router.get("/remove", async (_req, res) => { } // Read the script content - const script = fs.readFileSync(scriptPath, "utf8"); + let script = fs.readFileSync(scriptPath, "utf8"); + + // Convert line endings + script = script.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + + // Determine curl flags dynamically from settings for consistency + let curlFlags = "-s"; + try { + const settings = await prisma.settings.findFirst(); + if (settings && settings.ignore_ssl_self_signed === true) { + curlFlags = "-sk"; + } + } catch (_) {} + + // Prepend environment for CURL_FLAGS so script can use it if needed + const envPrefix = `#!/bin/bash\nexport CURL_FLAGS="${curlFlags}"\n\n`; + script = script.replace(/^#!/, "#"); + script = envPrefix + script; // Set appropriate headers for script download res.setHeader("Content-Type", "text/plain"); diff --git a/backend/src/routes/settingsRoutes.js b/backend/src/routes/settingsRoutes.js index e496726..0474e6d 100644 --- a/backend/src/routes/settingsRoutes.js +++ b/backend/src/routes/settingsRoutes.js @@ -165,19 +165,31 @@ router.put( requireManageSettings, [ body("serverProtocol") + .optional() .isIn(["http", "https"]) .withMessage("Protocol must be http or https"), body("serverHost") + .optional() .isLength({ min: 1 }) .withMessage("Server host is required"), body("serverPort") + .optional() .isInt({ min: 1, max: 65535 }) .withMessage("Port must be between 1 and 65535"), body("updateInterval") + .optional() .isInt({ min: 5, max: 1440 }) .withMessage("Update interval must be between 5 and 1440 minutes"), - body("autoUpdate").isBoolean().withMessage("Auto update must be a boolean"), + body("autoUpdate") + .optional() + .isBoolean() + .withMessage("Auto update must be a boolean"), + body("ignoreSslSelfSigned") + .optional() + .isBoolean() + .withMessage("Ignore SSL self-signed must be a boolean"), body("signupEnabled") + .optional() .isBoolean() .withMessage("Signup enabled must be a boolean"), body("defaultUserRole") @@ -218,6 +230,7 @@ router.put( serverPort, updateInterval, autoUpdate, + ignoreSslSelfSigned, signupEnabled, defaultUserRole, githubRepoUrl, @@ -229,32 +242,43 @@ router.put( const currentSettings = await getSettings(); const oldUpdateInterval = currentSettings.update_interval; - // Update settings using the service - const normalizedInterval = normalizeUpdateInterval(updateInterval || 60); + // Build update object with only provided fields + const updateData = {}; - const updatedSettings = await updateSettings(currentSettings.id, { - server_protocol: serverProtocol, - server_host: serverHost, - server_port: serverPort, - update_interval: normalizedInterval, - auto_update: autoUpdate || false, - signup_enabled: signupEnabled || false, - default_user_role: - defaultUserRole || process.env.DEFAULT_USER_ROLE || "user", - github_repo_url: - githubRepoUrl !== undefined - ? githubRepoUrl - : "git@github.com:9technologygroup/patchmon.net.git", - repository_type: repositoryType || "public", - ssh_key_path: sshKeyPath || null, - }); + if (serverProtocol !== undefined) + updateData.server_protocol = serverProtocol; + if (serverHost !== undefined) updateData.server_host = serverHost; + if (serverPort !== undefined) updateData.server_port = serverPort; + if (updateInterval !== undefined) { + updateData.update_interval = normalizeUpdateInterval(updateInterval); + } + if (autoUpdate !== undefined) updateData.auto_update = autoUpdate; + if (ignoreSslSelfSigned !== undefined) + updateData.ignore_ssl_self_signed = ignoreSslSelfSigned; + if (signupEnabled !== undefined) + updateData.signup_enabled = signupEnabled; + if (defaultUserRole !== undefined) + updateData.default_user_role = defaultUserRole; + if (githubRepoUrl !== undefined) + updateData.github_repo_url = githubRepoUrl; + if (repositoryType !== undefined) + updateData.repository_type = repositoryType; + if (sshKeyPath !== undefined) updateData.ssh_key_path = sshKeyPath; + + const updatedSettings = await updateSettings( + currentSettings.id, + updateData, + ); console.log("Settings updated successfully:", updatedSettings); // If update interval changed, trigger crontab updates on all hosts with auto-update enabled - if (oldUpdateInterval !== normalizedInterval) { + if ( + updateInterval !== undefined && + oldUpdateInterval !== updateData.update_interval + ) { console.log( - `Update interval changed from ${oldUpdateInterval} to ${normalizedInterval} minutes. Triggering crontab updates...`, + `Update interval changed from ${oldUpdateInterval} to ${updateData.update_interval} minutes. Triggering crontab updates...`, ); await triggerCrontabUpdates(); } diff --git a/backend/src/services/settingsService.js b/backend/src/services/settingsService.js index b81c541..3a137ef 100644 --- a/backend/src/services/settingsService.js +++ b/backend/src/services/settingsService.js @@ -43,6 +43,7 @@ async function createSettingsFromEnvironment() { update_interval: 60, auto_update: false, signup_enabled: false, + ignore_ssl_self_signed: false, updated_at: new Date(), }, }); diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 3c0de48..a2a096e 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -6,7 +6,6 @@ import { ChevronRight, Clock, Container, - FileText, GitBranch, Github, Globe, @@ -23,8 +22,6 @@ import { Shield, Star, UserCircle, - Users, - Wrench, X, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; diff --git a/frontend/src/components/SettingsLayout.jsx b/frontend/src/components/SettingsLayout.jsx index 32e2c85..5f67a26 100644 --- a/frontend/src/components/SettingsLayout.jsx +++ b/frontend/src/components/SettingsLayout.jsx @@ -1,13 +1,10 @@ import { - AlertTriangle, - ArrowLeft, Bell, ChevronLeft, ChevronRight, Code, Folder, RefreshCw, - Server, Settings, Shield, UserCircle, @@ -121,7 +118,7 @@ const SettingsLayout = ({ children }) => { const isActive = (path) => location.pathname === path; - const getPageTitle = () => { + const _getPageTitle = () => { const path = location.pathname; if (path.startsWith("/settings/users")) return "Users"; diff --git a/frontend/src/components/settings/AgentManagementTab.jsx b/frontend/src/components/settings/AgentManagementTab.jsx index 1c78707..60274fb 100644 --- a/frontend/src/components/settings/AgentManagementTab.jsx +++ b/frontend/src/components/settings/AgentManagementTab.jsx @@ -1,7 +1,7 @@ import { useMutation, useQuery } from "@tanstack/react-query"; import { AlertCircle, Code, Download, Plus, Shield, X } from "lucide-react"; import { useId, useState } from "react"; -import { agentFileAPI } from "../../utils/api"; +import { agentFileAPI, settingsAPI } from "../../utils/api"; const AgentManagementTab = () => { const scriptFileId = useId(); @@ -19,6 +19,17 @@ const AgentManagementTab = () => { queryFn: () => agentFileAPI.getInfo().then((res) => res.data), }); + // Fetch settings for dynamic curl flags + const { data: settings } = useQuery({ + queryKey: ["settings"], + queryFn: () => settingsAPI.get().then((res) => res.data), + }); + + // Helper function to get curl flags based on settings + const getCurlFlags = () => { + return settings?.ignore_ssl_self_signed ? "-sk" : "-s"; + }; + const uploadAgentMutation = useMutation({ mutationFn: (scriptContent) => agentFileAPI.upload(scriptContent).then((res) => res.data), @@ -171,13 +182,13 @@ const AgentManagementTab = () => {

- curl -ks {window.location.origin} + curl {getCurlFlags()} {window.location.origin} /api/v1/hosts/remove | sudo bash
+ {/* SSL Certificate Setting */} +
+ +

+ When enabled, curl commands in agent scripts will use the -k flag to + ignore SSL certificate validation errors. Use with caution on + production systems as this reduces security. +

+
+ {/* User Signup Setting */}