544 show version number (#548)
* Add version management endpoints to check for updates * Update version number to 0.1.5 and add version check functionality with banner notification * lint * Update README to reference docker-compose.yml version 0.1.5
taylor_socfortress committed
Dec 3, 2025 at 20:20 UTC
f0106cb57adb6ab4b41f9e25e87d6b346ef3724b
13 files changed
+315
-3
README.md
+1
-1
@@ -88,7 +88,7 @@ systemctl restart docker
88
89
```bash
90
# Clone the CoPilot repository
91
-wget https://raw.githubusercontent.com/socfortress/CoPilot/v0.1.4/docker-compose.yml
91
+wget https://raw.githubusercontent.com/socfortress/CoPilot/v0.1.5/docker-compose.yml
92
93
# Edit the docker-compose.yml file to set the server name and/or the services you want to use
94
backend/app/routers/version.py
new
+13
@@ -0,0 +1,13 @@
1
+from fastapi import APIRouter
2
+
3
+from app.version.routes.version import version_router
4
+
5
+# Instantiate the APIRouter
6
+router = APIRouter()
7
+
8
+# Include the Ask SocFortress related routes
9
+router.include_router(
10
+ version_router,
11
+ prefix="/version",
12
+ tags=["Version Management"],
13
+)
backend/app/version/routes/version.py
new
+28
@@ -0,0 +1,28 @@
1
+from fastapi import APIRouter
2
+from app.version.schema.version import VersionCheckResponse
3
+from app.version.services.version import check_version_outdated
4
+from loguru import logger
5
+
6
+version_router = APIRouter()
7
+
8
+@version_router.get(
9
+ "/check",
10
+ response_model=VersionCheckResponse,
11
+ description="Check if CoPilot version is up to date"
12
+)
13
+async def check_version() -> VersionCheckResponse:
14
+ """
15
+ Check if the current CoPilot version is outdated.
16
+
17
+ This endpoint compares the current version with the latest release
18
+ from the GitHub repository.
19
+
20
+ **Returns:**
21
+ - Current version
22
+ - Latest available version
23
+ - Whether an update is available
24
+ - Link to release notes if outdated
25
+ """
26
+ logger.info("Checking CoPilot version")
27
+ result = await check_version_outdated()
28
+ return VersionCheckResponse(**result)
backend/app/version/schema/version.py
new
+26
@@ -0,0 +1,26 @@
1
+from pydantic import BaseModel
2
+from typing import Optional
3
+
4
+class VersionCheckResponse(BaseModel):
5
+ success: bool
6
+ message: str
7
+ current_version: str
8
+ latest_version: Optional[str]
9
+ is_outdated: bool
10
+ release_url: Optional[str] = None
11
+ release_notes: Optional[str] = None
12
+ published_at: Optional[str] = None
13
+
14
+ class Config:
15
+ json_schema_extra = {
16
+ "example": {
17
+ "success": True,
18
+ "message": "New version v0.1.5 available!",
19
+ "current_version": "0.1.4",
20
+ "latest_version": "0.1.5",
21
+ "is_outdated": True,
22
+ "release_url": "https://github.com/socfortress/CoPilot/releases/tag/v0.1.5",
23
+ "release_notes": "## What's Changed\n* Feature 1\n* Feature 2",
24
+ "published_at": "2025-12-03T18:48:20Z"
25
+ }
26
+ }
backend/app/version/services/version.py
new
+82
@@ -0,0 +1,82 @@
1
+from packaging.version import Version
2
+from loguru import logger
3
+import httpx
4
+from typing import Optional, Dict, Any
5
+
6
+# Current version - update this with each release
7
+CURRENT_VERSION = "0.1.5"
8
+VERSION_CHECK_URL = "https://api.github.com/repos/socfortress/CoPilot/releases/latest"
9
+
10
+async def get_latest_version() -> Optional[Dict[str, Any]]:
11
+ """
12
+ Fetch the latest version from GitHub releases
13
+
14
+ Returns:
15
+ dict: Release information including version, url, and body or None if unable to fetch
16
+ """
17
+ try:
18
+ async with httpx.AsyncClient() as client:
19
+ response = await client.get(VERSION_CHECK_URL, timeout=5.0)
20
+ if response.status_code == 200:
21
+ data = response.json()
22
+ return {
23
+ "tag_name": data.get("tag_name", "").lstrip("v"),
24
+ "html_url": data.get("html_url", ""),
25
+ "published_at": data.get("published_at", ""),
26
+ "body": data.get("body", ""),
27
+ "name": data.get("name", "").lstrip("v")
28
+ }
29
+ except Exception as e:
30
+ logger.warning(f"Failed to fetch latest version: {e}")
31
+ return None
32
+
33
+async def check_version_outdated() -> dict:
34
+ """
35
+ Check if current version is outdated
36
+
37
+ Returns:
38
+ dict: Version check results
39
+ """
40
+ release_info = await get_latest_version()
41
+
42
+ if not release_info:
43
+ return {
44
+ "success": False,
45
+ "message": "Unable to check for updates",
46
+ "current_version": CURRENT_VERSION,
47
+ "latest_version": None,
48
+ "is_outdated": False,
49
+ "release_url": None,
50
+ "release_notes": None,
51
+ "published_at": None
52
+ }
53
+
54
+ latest_version = release_info.get("tag_name")
55
+
56
+ try:
57
+ current = Version(CURRENT_VERSION)
58
+ latest = Version(latest_version)
59
+ is_outdated = current < latest
60
+
61
+ return {
62
+ "success": True,
63
+ "message": f"New version v{latest_version} available!" if is_outdated else "You're up to date",
64
+ "current_version": CURRENT_VERSION,
65
+ "latest_version": latest_version,
66
+ "is_outdated": is_outdated,
67
+ "release_url": release_info.get("html_url"),
68
+ "release_notes": release_info.get("body"),
69
+ "published_at": release_info.get("published_at")
70
+ }
71
+ except Exception as e:
72
+ logger.error(f"Error comparing versions: {e}")
73
+ return {
74
+ "success": False,
75
+ "message": "Error checking version",
76
+ "current_version": CURRENT_VERSION,
77
+ "latest_version": latest_version,
78
+ "is_outdated": False,
79
+ "release_url": release_info.get("html_url"),
80
+ "release_notes": None,
81
+ "published_at": None
82
+ }
backend/copilot.py
+2
@@ -66,6 +66,7 @@ from app.routers import monitoring_alert
66
from app.routers import network_connectors
67
from app.routers import nuclei
68
from app.routers import office365
69
+from app.routers import version
70
from app.routers import portainer
71
from app.routers import sap_siem
72
from app.routers import scheduler
@@ -140,6 +141,7 @@ api_router.include_router(smtp.router)
141
api_router.include_router(dnstwist.router)
142
api_router.include_router(logs.router)
143
api_router.include_router(influxdb.router)
144
+api_router.include_router(version.router)
145
api_router.include_router(grafana.router)
146
api_router.include_router(customer_provisioning.router)
147
api_router.include_router(threat_intel.router)
frontend/src/api/endpoints/version.ts
new
+9
@@ -0,0 +1,9 @@
1
+import type { FlaskBaseResponse } from "@/types/flask.d"
2
+import type { VersionCheckResponse } from "@/types/version.d"
3
+import { HttpClient } from "../httpClient"
4
+
5
+export default {
6
+ checkVersion() {
7
+ return HttpClient.get<FlaskBaseResponse & VersionCheckResponse>(`/version/check`)
8
+ }
9
+}
frontend/src/api/index.ts
+3
-1
@@ -30,6 +30,7 @@ import stackProvisioning from "./endpoints/stackProvisioning"
30
import sysmonConfig from "./endpoints/sysmonConfig"
31
import threatIntel from "./endpoints/threatIntel"
32
import users from "./endpoints/users"
33
+import version from "./endpoints/version"
34
import vulnerabilities from "./endpoints/vulnerabilities"
35
import wazuh from "./endpoints/wazuh"
36
import indices from "./endpoints/wazuh/indices"
@@ -71,5 +72,6 @@ export default {
72
portainer,
73
shuffle,
74
copilotMCP,
74
- customerPortal
75
+ customerPortal,
76
+ version
77
}
frontend/src/app-layouts/HorizontalNav/MainContainer.vue
+1
@@ -15,6 +15,7 @@
15
}"
16
>
17
<Toolbar :boxed="toolbarBoxed" gradient="sidebar" />
18
+
19
<div
20
id="app-view"
21
class="view"
frontend/src/components/common/VersionUpdateBanner.vue
new
+134
@@ -0,0 +1,134 @@
1
+<template>
2
+ <n-alert
3
+ v-if="showBanner && versionInfo?.is_outdated"
4
+ type="info"
5
+ closable
6
+ class="mb-4"
7
+ @close="dismissBanner"
8
+ >
9
+ <template #icon>
10
+ <Icon :name="UpdateIcon" />
11
+ </template>
12
+ <div class="flex flex-col gap-3">
13
+ <div class="flex items-center justify-between gap-4">
14
+ <div>
15
+ <strong>New version available!</strong>
16
+ <div class="text-sm mt-1">
17
+ You're running v{{ versionInfo.current_version }},
18
+ v{{ versionInfo.latest_version }} is now available.
19
+ <span v-if="versionInfo.published_at" class="opacity-75">
20
+ (Released {{ formatDate(versionInfo.published_at) }})
21
+ </span>
22
+ </div>
23
+ </div>
24
+ <div class="flex gap-2">
25
+ <n-button
26
+ v-if="showReleaseNotes"
27
+ size="small"
28
+ @click="toggleNotes"
29
+ >
30
+ {{ expandedNotes ? 'Hide' : 'Show' }} Release Notes
31
+ </n-button>
32
+ <n-button
33
+ v-if="versionInfo.release_url"
34
+ tag="a"
35
+ :href="versionInfo.release_url"
36
+ target="_blank"
37
+ size="small"
38
+ type="primary"
39
+ >
40
+ View on GitHub
41
+ </n-button>
42
+ </div>
43
+ </div>
44
+ <n-collapse-transition :show="expandedNotes">
45
+ <div v-if="versionInfo.release_notes" class="release-notes text-sm">
46
+ <n-divider class="!my-2" />
47
+ <pre class="whitespace-pre-wrap">{{ versionInfo.release_notes }}</pre>
48
+ </div>
49
+ </n-collapse-transition>
50
+ </div>
51
+ </n-alert>
52
+</template>
53
+
54
+<script setup lang="ts">
55
+import type { VersionCheckResponse } from "@/types/version.d"
56
+import { NAlert, NButton, NCollapseTransition, NDivider } from "naive-ui"
57
+import { computed, onMounted, ref } from "vue"
58
+import Api from "@/api"
59
+import Icon from "@/components/common/Icon.vue"
60
+import dayjs from "@/utils/dayjs"
61
+
62
+const UpdateIcon = "carbon:update-now"
63
+const versionInfo = ref<VersionCheckResponse | null>(null)
64
+const showBanner = ref(false)
65
+const expandedNotes = ref(false)
66
+
67
+const DISMISS_KEY = "version_banner_dismissed"
68
+const DISMISS_DURATION = 24 * 60 * 60 * 1000 // 24 hours
69
+
70
+const showReleaseNotes = computed(() => {
71
+ return versionInfo.value?.release_notes && versionInfo.value.release_notes.trim().length > 0
72
+})
73
+
74
+function formatDate(dateString: string): string {
75
+ return dayjs(dateString).format("MMM D, YYYY")
76
+}
77
+
78
+function toggleNotes() {
79
+ expandedNotes.value = !expandedNotes.value
80
+}
81
+
82
+function checkIfDismissed() {
83
+ const dismissed = localStorage.getItem(DISMISS_KEY)
84
+ if (dismissed) {
85
+ const dismissedTime = Number.parseInt(dismissed)
86
+ const now = Date.now()
87
+ // Show again after 24 hours
88
+ return now - dismissedTime < DISMISS_DURATION
89
+ }
90
+ return false
91
+}
92
+
93
+function dismissBanner() {
94
+ localStorage.setItem(DISMISS_KEY, Date.now().toString())
95
+ showBanner.value = false
96
+}
97
+
98
+async function checkVersion() {
99
+ // Don't check if recently dismissed
100
+ if (checkIfDismissed()) {
101
+ return
102
+ }
103
+
104
+ try {
105
+ const response = await Api.version.checkVersion()
106
+ if (response.data.success) {
107
+ versionInfo.value = response.data
108
+ showBanner.value = response.data.is_outdated
109
+ }
110
+ } catch (err) {
111
+ // Silently fail - version check is not critical
112
+ console.warn("Failed to check version:", err)
113
+ }
114
+}
115
+
116
+onMounted(() => {
117
+ checkVersion()
118
+})
119
+</script>
120
+
121
+<style lang="scss" scoped>
122
+.release-notes {
123
+ max-height: 400px;
124
+ overflow-y: auto;
125
+ padding: 0.5rem;
126
+ border-radius: 4px;
127
+ background-color: rgba(0, 0, 0, 0.05);
128
+
129
+ pre {
130
+ font-family: inherit;
131
+ margin: 0;
132
+ }
133
+}
134
+</style>
frontend/src/components/incidentManagement/sources/SourceConfigurationForm.vue
-1
@@ -215,7 +215,6 @@ import {
215
NSelect,
216
NSpin,
217
NTag,
218
- NText,
218
useMessage
219
} from "naive-ui"
220
import { computed, h, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
frontend/src/types/version.d.ts
new
+10
@@ -0,0 +1,10 @@
1
+export interface VersionCheckResponse {
2
+ success: boolean
3
+ message: string
4
+ current_version: string
5
+ latest_version: string | null
6
+ is_outdated: boolean
7
+ release_url?: string
8
+ release_notes?: string
9
+ published_at?: string
10
+}
frontend/src/views/Overview.vue
+6
@@ -1,5 +1,10 @@
1
<template>
2
<div ref="page" class="page">
3
+ <!-- Add version banner at the top -->
4
+ <div class="section">
5
+ <VersionUpdateBanner />
6
+ </div>
7
+
8
<div class="section flex justify-end gap-3 lg:justify-between">
9
<div class="left-box hidden gap-3 lg:flex">
10
<StackProvisioningButton size="small" type="primary" />
@@ -82,6 +87,7 @@ import { defineAsyncComponent, ref } from "vue"
87
import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
88
import CloudSecurityAssessmentButton from "@/components/cloudSecurityAssessment/CloudSecurityAssessmentButton.vue"
89
import Icon from "@/components/common/Icon.vue"
90
+import VersionUpdateBanner from "@/components/common/VersionUpdateBanner.vue"
91
import PipeList from "@/components/graylog/Pipelines/PipeList.vue"
92
import ClusterHealth from "@/components/indices/ClusterHealth.vue"
93
import IndicesMarquee from "@/components/indices/Marquee.vue"