@cryptotaxi247 / CoPilot / commits / f0106cb5

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"