@cryptotaxi247 / CoPilot / commits / 6fb7aed5

License integration (#191)

* Update Docker configuration and remove unused code * license to socfortress middleware fixes * alot...need to clean alot of this up * Refactor add_license_to_db function call in create_trial_license_key * Fix logger error message in send_post_request function * Fix password regex in UserInput model * Add check for unique ports when provisioning Wazuh worker * Update branch name in GitHub Actions workflow * updated license api * updated license page * updated dependencies * updated license types * improved style * added license checkout component * updated license components * modules integrations * make a build for current branch * added price formatter * updated license features type * updated layout style * removed logs * updated license features component * updated license checkout wizard * build for frontend * Fix license key parameter in post_to_copilot_huntress_module * Add timeout for POST request to copilot-huntress-module * Add logging statement to post_to_copilot_huntress_module function * Refactor post_to_copilot_huntress_module to accept license_key as a parameter * Refactor Huntress module route * Refactor Huntress module routes and add helper functions * Fix post_to_copilot_huntress_module error handling * updated license checkout wizard * copilot huntress integration returns * Refactor post_to_copilot_huntress_module function * add results endpoint to receive module results * Add post_to_copilot_huntress_module function and update its usage * Refactor post_to_copilot_huntress_module to use async/await syntax * Refactor post_to_copilot_huntress_module function to use requests library * Refactor module_results_router.post endpoint * updated license checkout wizard * Add extra data configuration to CopilotResponse * Refactor post_to_copilot_huntress_module to use httpx for asynchronous requests * Add timeout parameter to post request in Huntress module * Remove unused code related to integration results * add the dfir_iris user configured within copilot to all customers within dfir_iris so copilot can view alerts for all customers * Remove commented out code in huntress.py * updated customer provision wizard added dfir_iris_username * retrieval of iris notes for a case complete for latest iris update * iris case note creation for newest iris version * bump iris to client to 2.0.4 * refactored router rules * added license cancel/success pages * Fix timestamp field mapping issue in collect_alerts_generic function * Update import paths for Huntress module in schedular * removed huntress collection things because this is now handled with CoPilot-Huntress-Module container * remove checkpoint * Add "filebeat" to the list of skipped indices * mimecast module integration * add docker compose module examples * Add Mimecast module router * Update success message in collect_huntress_route function * added license success/cancel page * added cancel subscription button * updated license page * carbon black integration added to catalog * carbonblack integration invoking * Add invoke_carbonblack_integration_collect to function_map * Add carbonblack router to APIRouter * Refactor get_license function in license.py*** * Add is_feature_enabled_route to license.py * Add CarbonBlack integration test route * Add time range parameter to carbonblack data collection * Update time range in carbonblack.py * added isFeatureEnabled api * added LicenseFeatureOverlay component * updated pinned page component * added report feature check * added LicenseLoadForm component * updated license page * Refactor integration execution logging * precommit fixes - ready to merge --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Apr 16, 2024 at 13:51 UTC 6fb7aed5fdbd259a74a167eb8d9676c790d13300
87 files changed +6433 -951
.github/workflows/docker.yml
-3
@@ -29,9 +29,6 @@ jobs:
29 push: true
30 tags: ghcr.io/socfortress/copilot-backend:latest
31 build-args: |
32 - CRYPTOLENS_AUTH=${{ secrets.CRYPTOLENS_AUTH }}
33 - RSA_PUBLIC_KEY=${{ secrets.RSA_PUBLIC_KEY }}
34 - PRODUCT_ID=${{ secrets.PRODUCT_ID }}
32 COPILOT_API_KEY=${{ secrets.COPILOT_API_KEY }}
33
34 build-frontend:
.vscode/settings.json
+3
@@ -3,9 +3,11 @@
3 "ajoelp",
4 "apexchart",
5 "arcticons",
6 + "CARBONBLACK",
7 "colord",
8 "datejs",
9 "datetimesec",
10 + "DFIR",
11 "echarts",
12 "firedtimes",
13 "forgotpassword",
@@ -16,6 +18,7 @@
18 "mimecast",
19 "mynaui",
20 "picocolors",
21 + "popconfirm",
22 "redoc",
23 "rushstack",
24 "signin",
backend/Dockerfile
-9
@@ -109,15 +109,6 @@ ENV GELF_INPUT_PORT=gelf_port
109
110 ENV ALERT_CREATION_PROVISIONING_URL=http://1.1.1.1
111
112 -ARG CRYPTOLENS_AUTH
113 -ENV CRYPTOLENS_AUTH=$CRYPTOLENS_AUTH
114 -
115 -ARG RSA_PUBLIC_KEY
116 -ENV RSA_PUBLIC_KEY=$RSA_PUBLIC_KEY
117 -
118 -ARG PRODUCT_ID
119 -ENV PRODUCT_ID=$PRODUCT_ID
120 -
112 ARG COPILOT_API_KEY
113 ENV COPILOT_API_KEY=$COPILOT_API_KEY
114
backend/app/auth/models/users.py
+1 -1
@@ -46,7 +46,7 @@ class UserInput(SQLModel):
46 password: str = Field(
47 max_length=256,
48 min_length=8,
49 - regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&#])[A-Za-z\\d@$!%*?&#]{8,}$",
49 + regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&#]).{8,}$",
50 description="Password must be at least 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one special character",
51 )
52 email: EmailStr
backend/app/connectors/dfir_iris/routes/users.py
+36
@@ -7,12 +7,15 @@ from loguru import logger
7 from app.auth.utils import AuthHandler
8 from app.connectors.dfir_iris.schema.alerts import AlertResponse
9 from app.connectors.dfir_iris.schema.users import User
10 +from app.connectors.dfir_iris.schema.users import UserAddedToCustomerResponse
11 from app.connectors.dfir_iris.schema.users import UsersResponse
12 from app.connectors.dfir_iris.services.users import assign_user_to_alert
13 from app.connectors.dfir_iris.services.users import delete_user_from_alert
14 from app.connectors.dfir_iris.services.users import get_users
15 +from app.connectors.dfir_iris.utils.universal import add_user_to_customers
16 from app.connectors.dfir_iris.utils.universal import check_alert_exists
17 from app.connectors.dfir_iris.utils.universal import check_user_exists
18 +from app.connectors.dfir_iris.utils.universal import collect_all_customers
19
20
21 def verify_user_exists(user_id: int) -> int:
@@ -98,6 +101,39 @@ async def assign_user_to_alert_route(
101 return await assign_user_to_alert(alert_id, user_id)
102
103
104 +@dfir_iris_users_router.post(
105 + "/add/{user_id}",
106 + response_model=AlertResponse,
107 + description="Add a user to a list of customers",
108 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
109 +)
110 +async def add_user_to_customers_route(
111 + user_id: int,
112 +) -> UserAddedToCustomerResponse:
113 + """
114 + Add a user to a list of customers.
115 +
116 + Parameters:
117 + - customers (List[str]): The list of customer IDs.
118 + - user_id (int): The ID of the user.
119 +
120 + Returns:
121 + - AlertResponse: The response containing the added user.
122 +
123 + Raises:
124 + - HTTPException: If the customer or user does not exist.
125 + """
126 + customers = await collect_all_customers()
127 + customer_ids = [str(customer["customer_id"]) for customer in customers]
128 + logger.info(f"Customer IDs: {customer_ids}")
129 + logger.info(f"Adding user {user_id} to customers {customer_ids}")
130 + success = await add_user_to_customers(customer_ids, user_id)
131 + if success:
132 + return UserAddedToCustomerResponse(message=f"User {user_id} added to customers {customers}", success=True)
133 + else:
134 + raise HTTPException(status_code=400, detail=f"Failed to add user {user_id} to customers {customers}")
135 +
136 +
137 @dfir_iris_users_router.delete(
138 "/assign/{alert_id}/{user_id}",
139 response_model=AlertResponse,
backend/app/connectors/dfir_iris/schema/notes.py
+21 -12
@@ -6,22 +6,33 @@ from pydantic import BaseModel
6 from pydantic import Field
7
8
9 -class CustomAttributes(BaseModel):
10 - # Define additional fields if custom_attributes contains specific keys
11 - pass
9 +class Directory(BaseModel):
10 + id: int
11 + name: str
12 + parent_id: Optional[int]
13 + case_id: int
14 +
15 +
16 +class ModificationHistory(BaseModel):
17 + user: str
18 + user_id: int
19 + action: str
20
21
22 class NoteDetails(BaseModel):
15 - custom_attributes: CustomAttributes
16 - group_id: int
17 - group_title: str
18 - group_uuid: str
23 + directory: Directory
24 + note_id: int
25 + note_uuid: str
26 + note_title: str
27 note_content: str
28 + note_user: int
29 note_creationdate: str
21 - note_id: int
30 note_lastupdate: str
23 - note_title: str
24 - note_uuid: str
31 + note_case_id: int
32 + custom_attributes: Optional[Dict]
33 + directory_id: int
34 + modification_history: Dict[str, ModificationHistory]
35 + comments: List[str]
36
37
38 class NoteDetailsResponse(BaseModel):
@@ -32,8 +43,6 @@ class NoteDetailsResponse(BaseModel):
43
44 class NoteItem(BaseModel):
45 note_details: NoteDetails
35 - note_id: int
36 - note_title: str
46
47
48 class NotesResponse(BaseModel):
backend/app/connectors/dfir_iris/schema/users.py
+5
@@ -15,3 +15,8 @@ class UsersResponse(BaseModel):
15 message: str
16 success: bool
17 users: List[User]
18 +
19 +
20 +class UserAddedToCustomerResponse(BaseModel):
21 + success: bool
22 + message: str
backend/app/connectors/dfir_iris/services/notes.py
+19 -14
@@ -14,7 +14,7 @@ from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
14 from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
15
16
17 -async def process_notes(notes: List[Dict], case_id: int) -> List[Dict]:
17 +async def process_directories(directories: List[Dict], case_id: int) -> List[Dict]:
18 """
19 Process a list of notes for a given case.
20
@@ -26,11 +26,13 @@ async def process_notes(notes: List[Dict], case_id: int) -> List[Dict]:
26 List[Dict]: The processed list of notes.
27 """
28 processed_notes = []
29 - for note in notes:
30 - note_details = await get_case_note_details(note["note_id"], case_id)
31 - logger.info(f"Note details: {note_details}")
32 - note["note_details"] = note_details.note_details
33 - processed_notes.append(note)
29 + for directory in directories:
30 + for note in directory["notes"]:
31 + logger.info(f"Note: {note}")
32 + note_details = await get_case_note_details(note["id"], case_id)
33 + logger.info(f"Note details: {note_details}")
34 + note["note_details"] = note_details.note_details
35 + processed_notes.append(note)
36 return processed_notes
37
38
@@ -48,11 +50,11 @@ async def get_case_notes(case_id: int, search_term: str) -> NotesResponse:
50 client, case = await initialize_client_and_case("DFIR-IRIS")
51 result = await fetch_and_validate_data(
52 client,
51 - case.search_notes,
52 - search_term,
53 + case.list_notes_directories,
54 case_id,
55 )
55 - processed_notes = await process_notes(result["data"], case_id)
56 + logger.info(f"Result: {result}")
57 + processed_notes = await process_directories(result["data"], case_id)
58 return NotesResponse(
59 success=True,
60 message="Successfully fetched notes for case",
@@ -60,7 +62,7 @@ async def get_case_notes(case_id: int, search_term: str) -> NotesResponse:
62 )
63
64
63 -async def get_case_note_details(note_id: int, case_id: int) -> NoteDetailsResponse:
65 +async def get_case_note_details(directory_id: int, case_id: int) -> NoteDetailsResponse:
66 """
67 Retrieves the details of a specific case note.
68
@@ -75,7 +77,8 @@ async def get_case_note_details(note_id: int, case_id: int) -> NoteDetailsRespon
77 SomeException: If there is an error retrieving the note details.
78 """
79 client, case = await initialize_client_and_case("DFIR-IRIS")
78 - result = await fetch_and_validate_data(client, case.get_note, note_id, case_id)
80 + result = await fetch_and_validate_data(client, case.get_note, directory_id, case_id)
81 + logger.info(f"Result: {result}")
82 note_details = NoteDetails(**result["data"])
83 return NoteDetailsResponse(
84 success=True,
@@ -104,11 +107,13 @@ async def perform_note_creation(
107 """
108 result = await fetch_and_validate_data(
109 client,
107 - case.add_notes_group,
108 - note_creation_body.note_title,
110 + case.add_notes_directory,
111 + "CoPilot",
112 + None,
113 case_id,
114 )
111 - note_id = result["data"]["group_id"]
115 + logger.info(f"Result: {result}")
116 + note_id = result["data"]["id"]
117 custom_attributes = {}
118 return await fetch_and_validate_data(
119 client,
backend/app/connectors/dfir_iris/utils/universal.py
+57
@@ -1,6 +1,7 @@
1 from typing import Any
2 from typing import Callable
3 from typing import Dict
4 +from typing import List
5 from typing import Optional
6 from typing import Tuple
7 from typing import Union
@@ -332,3 +333,59 @@ async def check_user_exists(user_id: int) -> bool:
333 except Exception as e:
334 logger.error(f"Failed to check if user {user_id} exists: {e}")
335 return False
336 +
337 +
338 +async def collect_all_customers() -> List[str]:
339 + """
340 + Collects all customers from the DFIR-IRIS system.
341 +
342 + Returns:
343 + List[str]: A list of customer IDs.
344 + """
345 + try:
346 + dfir_iris_client = await create_dfir_iris_client("DFIR-IRIS")
347 + customers = Customer(session=dfir_iris_client).list_customers()
348 + assert_api_resp(customers, soft_fail=False)
349 + customers = get_data_from_resp(customers)
350 + logger.info(f"Collected all customers: {customers}")
351 + return customers
352 + except Exception as e:
353 + logger.error(f"Failed to collect all customers: {e}")
354 + return []
355 +
356 +
357 +async def add_user_to_customers(customers: List[str], user_id: int) -> bool:
358 + """
359 + Add a user to a list of customers.
360 +
361 + Args:
362 + customers (List[int]): The list of customer IDs.
363 + user_id (int): The ID of the user to add.
364 +
365 + Returns:
366 + bool: True if the user was added successfully, False otherwise.
367 + """
368 + try:
369 + logger.info(f"Adding user {user_id} to customers {customers}")
370 + async with get_db_session() as session: # This will correctly enter the context manager
371 + attributes = await get_connector_info_from_db("DFIR-IRIS", session)
372 + headers = {
373 + "Authorization": f"Bearer {attributes['connector_api_key']}",
374 + }
375 + logger.info(f"Headers: {headers}")
376 + data = {
377 + "customers_membership": customers,
378 + }
379 + url_endpoint = f"{attributes['connector_url']}/manage/users/{user_id}/customers/update"
380 +
381 + response = requests.post(url_endpoint, headers=headers, json=data, verify=False)
382 + logger.info(f"Response: {response.json()}")
383 + if response.status_code == 200:
384 + logger.info(f"User {user_id} added to customers {customers}")
385 + return True
386 + else:
387 + logger.error(f"Failed to add user {user_id} to customers {customers}")
388 + return False
389 + except Exception as e:
390 + logger.error(f"Failed to add user {user_id} to customers {customers}: {e}")
391 + return False
backend/app/connectors/grafana/dashboards/CarbonBlack/summary.json new
+1891
@@ -0,0 +1,1891 @@
1 +{
2 + "annotations": {
3 + "list": [
4 + {
5 + "builtIn": 1,
6 + "datasource": {
7 + "type": "grafana",
8 + "uid": "-- Grafana --"
9 + },
10 + "enable": true,
11 + "hide": true,
12 + "iconColor": "rgba(0, 211, 255, 1)",
13 + "name": "Annotations & Alerts",
14 + "type": "dashboard"
15 + }
16 + ]
17 + },
18 + "editable": false,
19 + "fiscalYearStartMonth": 0,
20 + "graphTooltip": 0,
21 + "id": null,
22 + "links": [],
23 + "panels": [
24 + {
25 + "datasource": {
26 + "type": "grafana-opensearch-datasource",
27 + "uid": "replace_datasource_uid"
28 + },
29 + "fieldConfig": {
30 + "defaults": {
31 + "mappings": [
32 + {
33 + "options": {
34 + "match": "null",
35 + "result": {
36 + "text": "N/A"
37 + }
38 + },
39 + "type": "special"
40 + }
41 + ],
42 + "thresholds": {
43 + "mode": "absolute",
44 + "steps": [
45 + {
46 + "color": "red",
47 + "value": null
48 + }
49 + ]
50 + },
51 + "unit": "locale"
52 + },
53 + "overrides": []
54 + },
55 + "gridPos": {
56 + "h": 6,
57 + "w": 3,
58 + "x": 0,
59 + "y": 0
60 + },
61 + "id": 3,
62 + "options": {
63 + "colorMode": "value",
64 + "graphMode": "area",
65 + "justifyMode": "auto",
66 + "orientation": "horizontal",
67 + "reduceOptions": {
68 + "calcs": [
69 + "sum"
70 + ],
71 + "fields": "",
72 + "values": false
73 + },
74 + "showPercentChange": false,
75 + "text": {},
76 + "textMode": "auto",
77 + "wideLayout": true
78 + },
79 + "pluginVersion": "10.4.1",
80 + "targets": [
81 + {
82 + "bucketAggs": [
83 + {
84 + "$$hashKey": "object:331",
85 + "field": "timestamp",
86 + "id": "2",
87 + "settings": {
88 + "interval": "auto",
89 + "min_doc_count": 0,
90 + "trimEdges": 0
91 + },
92 + "type": "date_histogram"
93 + }
94 + ],
95 + "datasource": {
96 + "type": "grafana-opensearch-datasource",
97 + "uid": "replace_datasource_uid"
98 + },
99 + "metrics": [
100 + {
101 + "$$hashKey": "object:329",
102 + "field": "select field",
103 + "id": "1",
104 + "type": "count"
105 + }
106 + ],
107 + "query": "device_name:$device AND severity:$severity AND mdr_alert:true",
108 + "refId": "A",
109 + "timeField": "timestamp"
110 + }
111 + ],
112 + "title": "MDR ALERTS (TOTAL)",
113 + "type": "stat"
114 + },
115 + {
116 + "datasource": {
117 + "type": "grafana-opensearch-datasource",
118 + "uid": "replace_datasource_uid"
119 + },
120 + "fieldConfig": {
121 + "defaults": {
122 + "color": {
123 + "mode": "palette-classic"
124 + },
125 + "custom": {
126 + "hideFrom": {
127 + "legend": false,
128 + "tooltip": false,
129 + "viz": false
130 + }
131 + },
132 + "decimals": 0,
133 + "mappings": [],
134 + "unit": "short"
135 + },
136 + "overrides": [
137 + {
138 + "matcher": {
139 + "id": "byName",
140 + "options": "1"
141 + },
142 + "properties": [
143 + {
144 + "id": "color",
145 + "value": {
146 + "fixedColor": "#C8F2C2",
147 + "mode": "fixed"
148 + }
149 + }
150 + ]
151 + },
152 + {
153 + "matcher": {
154 + "id": "byName",
155 + "options": "2"
156 + },
157 + "properties": [
158 + {
159 + "id": "color",
160 + "value": {
161 + "fixedColor": "#96D98D",
162 + "mode": "fixed"
163 + }
164 + }
165 + ]
166 + },
167 + {
168 + "matcher": {
169 + "id": "byName",
170 + "options": "3"
171 + },
172 + "properties": [
173 + {
174 + "id": "color",
175 + "value": {
176 + "fixedColor": "#56A64B",
177 + "mode": "fixed"
178 + }
179 + }
180 + ]
181 + },
182 + {
183 + "matcher": {
184 + "id": "byName",
185 + "options": "4"
186 + },
187 + "properties": [
188 + {
189 + "id": "color",
190 + "value": {
191 + "fixedColor": "#37872D",
192 + "mode": "fixed"
193 + }
194 + }
195 + ]
196 + },
197 + {
198 + "matcher": {
199 + "id": "byName",
200 + "options": "5"
201 + },
202 + "properties": [
203 + {
204 + "id": "color",
205 + "value": {
206 + "fixedColor": "#FFF899",
207 + "mode": "fixed"
208 + }
209 + }
210 + ]
211 + },
212 + {
213 + "matcher": {
214 + "id": "byName",
215 + "options": "7"
216 + },
217 + "properties": [
218 + {
219 + "id": "color",
220 + "value": {
221 + "fixedColor": "#F2CC0C",
222 + "mode": "fixed"
223 + }
224 + }
225 + ]
226 + },
227 + {
228 + "matcher": {
229 + "id": "byName",
230 + "options": "9"
231 + },
232 + "properties": [
233 + {
234 + "id": "color",
235 + "value": {
236 + "fixedColor": "#FF9830",
237 + "mode": "fixed"
238 + }
239 + }
240 + ]
241 + },
242 + {
243 + "matcher": {
244 + "id": "byName",
245 + "options": "10"
246 + },
247 + "properties": [
248 + {
249 + "id": "color",
250 + "value": {
251 + "fixedColor": "#FF9830",
252 + "mode": "fixed"
253 + }
254 + }
255 + ]
256 + },
257 + {
258 + "matcher": {
259 + "id": "byName",
260 + "options": "12"
261 + },
262 + "properties": [
263 + {
264 + "id": "color",
265 + "value": {
266 + "fixedColor": "#F2495C",
267 + "mode": "fixed"
268 + }
269 + }
270 + ]
271 + },
272 + {
273 + "matcher": {
274 + "id": "byName",
275 + "options": "13"
276 + },
277 + "properties": [
278 + {
279 + "id": "color",
280 + "value": {
281 + "fixedColor": "#FF7383",
282 + "mode": "fixed"
283 + }
284 + }
285 + ]
286 + }
287 + ]
288 + },
289 + "gridPos": {
290 + "h": 12,
291 + "w": 4,
292 + "x": 3,
293 + "y": 0
294 + },
295 + "id": 7,
296 + "maxDataPoints": 3,
297 + "options": {
298 + "displayLabels": [],
299 + "legend": {
300 + "calcs": [],
301 + "displayMode": "table",
302 + "placement": "right",
303 + "showLegend": true,
304 + "values": [
305 + "value",
306 + "percent"
307 + ]
308 + },
309 + "pieType": "donut",
310 + "reduceOptions": {
311 + "calcs": [
312 + "sum"
313 + ],
314 + "fields": "",
315 + "values": false
316 + },
317 + "text": {},
318 + "tooltip": {
319 + "mode": "single",
320 + "sort": "none"
321 + }
322 + },
323 + "targets": [
324 + {
325 + "bucketAggs": [
326 + {
327 + "$$hashKey": "object:235",
328 + "fake": true,
329 + "field": "severity",
330 + "id": "3",
331 + "settings": {
332 + "min_doc_count": 1,
333 + "order": "desc",
334 + "orderBy": "_count",
335 + "size": "10"
336 + },
337 + "type": "terms"
338 + },
339 + {
340 + "$$hashKey": "object:236",
341 + "field": "timestamp",
342 + "id": "2",
343 + "settings": {
344 + "interval": "auto",
345 + "min_doc_count": 0,
346 + "trimEdges": 0
347 + },
348 + "type": "date_histogram"
349 + }
350 + ],
351 + "datasource": {
352 + "type": "grafana-opensearch-datasource",
353 + "uid": "replace_datasource_uid"
354 + },
355 + "metrics": [
356 + {
357 + "$$hashKey": "object:233",
358 + "field": "select field",
359 + "id": "1",
360 + "meta": {},
361 + "settings": {},
362 + "type": "count"
363 + }
364 + ],
365 + "query": "device_name:$device AND severity:$severity",
366 + "refId": "A",
367 + "timeField": "timestamp"
368 + }
369 + ],
370 + "title": "SECURITY EVENTS BY SEVERITY",
371 + "type": "piechart"
372 + },
373 + {
374 + "datasource": {
375 + "type": "grafana-opensearch-datasource",
376 + "uid": "replace_datasource_uid"
377 + },
378 + "fieldConfig": {
379 + "defaults": {
380 + "color": {
381 + "mode": "palette-classic"
382 + },
383 + "custom": {
384 + "hideFrom": {
385 + "legend": false,
386 + "tooltip": false,
387 + "viz": false
388 + }
389 + },
390 + "decimals": 0,
391 + "mappings": [],
392 + "unit": "short"
393 + },
394 + "overrides": [
395 + {
396 + "matcher": {
397 + "id": "byName",
398 + "options": "1"
399 + },
400 + "properties": [
401 + {
402 + "id": "color",
403 + "value": {
404 + "fixedColor": "#C8F2C2",
405 + "mode": "fixed"
406 + }
407 + }
408 + ]
409 + },
410 + {
411 + "matcher": {
412 + "id": "byName",
413 + "options": "2"
414 + },
415 + "properties": [
416 + {
417 + "id": "color",
418 + "value": {
419 + "fixedColor": "#96D98D",
420 + "mode": "fixed"
421 + }
422 + }
423 + ]
424 + },
425 + {
426 + "matcher": {
427 + "id": "byName",
428 + "options": "3"
429 + },
430 + "properties": [
431 + {
432 + "id": "color",
433 + "value": {
434 + "fixedColor": "#56A64B",
435 + "mode": "fixed"
436 + }
437 + }
438 + ]
439 + },
440 + {
441 + "matcher": {
442 + "id": "byName",
443 + "options": "4"
444 + },
445 + "properties": [
446 + {
447 + "id": "color",
448 + "value": {
449 + "fixedColor": "#37872D",
450 + "mode": "fixed"
451 + }
452 + }
453 + ]
454 + },
455 + {
456 + "matcher": {
457 + "id": "byName",
458 + "options": "5"
459 + },
460 + "properties": [
461 + {
462 + "id": "color",
463 + "value": {
464 + "fixedColor": "#FFF899",
465 + "mode": "fixed"
466 + }
467 + }
468 + ]
469 + },
470 + {
471 + "matcher": {
472 + "id": "byName",
473 + "options": "7"
474 + },
475 + "properties": [
476 + {
477 + "id": "color",
478 + "value": {
479 + "fixedColor": "#F2CC0C",
480 + "mode": "fixed"
481 + }
482 + }
483 + ]
484 + },
485 + {
486 + "matcher": {
487 + "id": "byName",
488 + "options": "9"
489 + },
490 + "properties": [
491 + {
492 + "id": "color",
493 + "value": {
494 + "fixedColor": "#FF9830",
495 + "mode": "fixed"
496 + }
497 + }
498 + ]
499 + },
500 + {
501 + "matcher": {
502 + "id": "byName",
503 + "options": "10"
504 + },
505 + "properties": [
506 + {
507 + "id": "color",
508 + "value": {
509 + "fixedColor": "#FF9830",
510 + "mode": "fixed"
511 + }
512 + }
513 + ]
514 + },
515 + {
516 + "matcher": {
517 + "id": "byName",
518 + "options": "12"
519 + },
520 + "properties": [
521 + {
522 + "id": "color",
523 + "value": {
524 + "fixedColor": "#F2495C",
525 + "mode": "fixed"
526 + }
527 + }
528 + ]
529 + },
530 + {
531 + "matcher": {
532 + "id": "byName",
533 + "options": "13"
534 + },
535 + "properties": [
536 + {
537 + "id": "color",
538 + "value": {
539 + "fixedColor": "#FF7383",
540 + "mode": "fixed"
541 + }
542 + }
543 + ]
544 + }
545 + ]
546 + },
547 + "gridPos": {
548 + "h": 12,
549 + "w": 4,
550 + "x": 7,
551 + "y": 0
552 + },
553 + "id": 8,
554 + "maxDataPoints": 3,
555 + "options": {
556 + "displayLabels": [],
557 + "legend": {
558 + "calcs": [],
559 + "displayMode": "table",
560 + "placement": "right",
561 + "showLegend": true,
562 + "values": [
563 + "value",
564 + "percent"
565 + ]
566 + },
567 + "pieType": "donut",
568 + "reduceOptions": {
569 + "calcs": [
570 + "sum"
571 + ],
572 + "fields": "",
573 + "values": false
574 + },
575 + "text": {},
576 + "tooltip": {
577 + "mode": "single",
578 + "sort": "none"
579 + }
580 + },
581 + "targets": [
582 + {
583 + "bucketAggs": [
584 + {
585 + "$$hashKey": "object:235",
586 + "fake": true,
587 + "field": "attack_tactic",
588 + "id": "3",
589 + "settings": {
590 + "min_doc_count": 1,
591 + "order": "desc",
592 + "orderBy": "_count",
593 + "size": "10"
594 + },
595 + "type": "terms"
596 + },
597 + {
598 + "$$hashKey": "object:236",
599 + "field": "timestamp",
600 + "id": "2",
601 + "settings": {
602 + "interval": "auto",
603 + "min_doc_count": 0,
604 + "trimEdges": 0
605 + },
606 + "type": "date_histogram"
607 + }
608 + ],
609 + "datasource": {
610 + "type": "grafana-opensearch-datasource",
611 + "uid": "replace_datasource_uid"
612 + },
613 + "metrics": [
614 + {
615 + "$$hashKey": "object:233",
616 + "field": "select field",
617 + "id": "1",
618 + "meta": {},
619 + "settings": {},
620 + "type": "count"
621 + }
622 + ],
623 + "query": "device_name:$device AND severity:$severity",
624 + "refId": "A",
625 + "timeField": "timestamp"
626 + }
627 + ],
628 + "title": "SECURITY EVENTS BY MITRE ID",
629 + "type": "piechart"
630 + },
631 + {
632 + "datasource": {
633 + "type": "grafana-opensearch-datasource",
634 + "uid": "replace_datasource_uid"
635 + },
636 + "fieldConfig": {
637 + "defaults": {
638 + "color": {
639 + "mode": "palette-classic"
640 + },
641 + "custom": {
642 + "hideFrom": {
643 + "legend": false,
644 + "tooltip": false,
645 + "viz": false
646 + }
647 + },
648 + "decimals": 0,
649 + "mappings": [],
650 + "unit": "short"
651 + },
652 + "overrides": [
653 + {
654 + "matcher": {
655 + "id": "byName",
656 + "options": "1"
657 + },
658 + "properties": [
659 + {
660 + "id": "color",
661 + "value": {
662 + "fixedColor": "#C8F2C2",
663 + "mode": "fixed"
664 + }
665 + }
666 + ]
667 + },
668 + {
669 + "matcher": {
670 + "id": "byName",
671 + "options": "2"
672 + },
673 + "properties": [
674 + {
675 + "id": "color",
676 + "value": {
677 + "fixedColor": "#96D98D",
678 + "mode": "fixed"
679 + }
680 + }
681 + ]
682 + },
683 + {
684 + "matcher": {
685 + "id": "byName",
686 + "options": "3"
687 + },
688 + "properties": [
689 + {
690 + "id": "color",
691 + "value": {
692 + "fixedColor": "#56A64B",
693 + "mode": "fixed"
694 + }
695 + }
696 + ]
697 + },
698 + {
699 + "matcher": {
700 + "id": "byName",
701 + "options": "4"
702 + },
703 + "properties": [
704 + {
705 + "id": "color",
706 + "value": {
707 + "fixedColor": "#37872D",
708 + "mode": "fixed"
709 + }
710 + }
711 + ]
712 + },
713 + {
714 + "matcher": {
715 + "id": "byName",
716 + "options": "5"
717 + },
718 + "properties": [
719 + {
720 + "id": "color",
721 + "value": {
722 + "fixedColor": "#FFF899",
723 + "mode": "fixed"
724 + }
725 + }
726 + ]
727 + },
728 + {
729 + "matcher": {
730 + "id": "byName",
731 + "options": "7"
732 + },
733 + "properties": [
734 + {
735 + "id": "color",
736 + "value": {
737 + "fixedColor": "#F2CC0C",
738 + "mode": "fixed"
739 + }
740 + }
741 + ]
742 + },
743 + {
744 + "matcher": {
745 + "id": "byName",
746 + "options": "9"
747 + },
748 + "properties": [
749 + {
750 + "id": "color",
751 + "value": {
752 + "fixedColor": "#FF9830",
753 + "mode": "fixed"
754 + }
755 + }
756 + ]
757 + },
758 + {
759 + "matcher": {
760 + "id": "byName",
761 + "options": "10"
762 + },
763 + "properties": [
764 + {
765 + "id": "color",
766 + "value": {
767 + "fixedColor": "#FF9830",
768 + "mode": "fixed"
769 + }
770 + }
771 + ]
772 + },
773 + {
774 + "matcher": {
775 + "id": "byName",
776 + "options": "12"
777 + },
778 + "properties": [
779 + {
780 + "id": "color",
781 + "value": {
782 + "fixedColor": "#F2495C",
783 + "mode": "fixed"
784 + }
785 + }
786 + ]
787 + },
788 + {
789 + "matcher": {
790 + "id": "byName",
791 + "options": "13"
792 + },
793 + "properties": [
794 + {
795 + "id": "color",
796 + "value": {
797 + "fixedColor": "#FF7383",
798 + "mode": "fixed"
799 + }
800 + }
801 + ]
802 + },
803 + {
804 + "matcher": {
805 + "id": "byName",
806 + "options": "ALLOW"
807 + },
808 + "properties": [
809 + {
810 + "id": "color",
811 + "value": {
812 + "fixedColor": "super-light-red",
813 + "mode": "fixed"
814 + }
815 + }
816 + ]
817 + },
818 + {
819 + "matcher": {
820 + "id": "byName",
821 + "options": "DENY"
822 + },
823 + "properties": [
824 + {
825 + "id": "color",
826 + "value": {
827 + "fixedColor": "green",
828 + "mode": "fixed"
829 + }
830 + }
831 + ]
832 + }
833 + ]
834 + },
835 + "gridPos": {
836 + "h": 12,
837 + "w": 4,
838 + "x": 11,
839 + "y": 0
840 + },
841 + "id": 12,
842 + "maxDataPoints": 3,
843 + "options": {
844 + "displayLabels": [],
845 + "legend": {
846 + "calcs": [],
847 + "displayMode": "table",
848 + "placement": "right",
849 + "showLegend": true,
850 + "values": [
851 + "value",
852 + "percent"
853 + ]
854 + },
855 + "pieType": "donut",
856 + "reduceOptions": {
857 + "calcs": [
858 + "sum"
859 + ],
860 + "fields": "",
861 + "values": false
862 + },
863 + "text": {},
864 + "tooltip": {
865 + "mode": "single",
866 + "sort": "none"
867 + }
868 + },
869 + "targets": [
870 + {
871 + "bucketAggs": [
872 + {
873 + "$$hashKey": "object:235",
874 + "fake": true,
875 + "field": "sensor_action",
876 + "id": "3",
877 + "settings": {
878 + "min_doc_count": 1,
879 + "order": "desc",
880 + "orderBy": "_count",
881 + "size": "10"
882 + },
883 + "type": "terms"
884 + },
885 + {
886 + "$$hashKey": "object:236",
887 + "field": "timestamp",
888 + "id": "2",
889 + "settings": {
890 + "interval": "auto",
891 + "min_doc_count": 0,
892 + "trimEdges": 0
893 + },
894 + "type": "date_histogram"
895 + }
896 + ],
897 + "datasource": {
898 + "type": "grafana-opensearch-datasource",
899 + "uid": "replace_datasource_uid"
900 + },
901 + "metrics": [
902 + {
903 + "$$hashKey": "object:233",
904 + "field": "select field",
905 + "id": "1",
906 + "meta": {},
907 + "settings": {},
908 + "type": "count"
909 + }
910 + ],
911 + "query": "device_name:$device AND severity:$severity",
912 + "refId": "A",
913 + "timeField": "timestamp"
914 + }
915 + ],
916 + "title": "SECURITY EVENTS BY SENSOR ACTION",
917 + "type": "piechart"
918 + },
919 + {
920 + "datasource": {
921 + "type": "grafana-opensearch-datasource",
922 + "uid": "replace_datasource_uid"
923 + },
924 + "fieldConfig": {
925 + "defaults": {
926 + "mappings": [],
927 + "thresholds": {
928 + "mode": "absolute",
929 + "steps": [
930 + {
931 + "color": "green",
932 + "value": null
933 + },
934 + {
935 + "color": "red",
936 + "value": 80
937 + }
938 + ]
939 + }
940 + },
941 + "overrides": []
942 + },
943 + "gridPos": {
944 + "h": 12,
945 + "w": 9,
946 + "x": 15,
947 + "y": 0
948 + },
949 + "id": 9,
950 + "options": {
951 + "displayMode": "gradient",
952 + "maxVizHeight": 300,
953 + "minVizHeight": 10,
954 + "minVizWidth": 0,
955 + "namePlacement": "auto",
956 + "orientation": "horizontal",
957 + "reduceOptions": {
958 + "calcs": [
959 + "sum"
960 + ],
961 + "fields": "",
962 + "values": false
963 + },
964 + "showUnfilled": true,
965 + "sizing": "auto",
966 + "text": {},
967 + "valueMode": "color"
968 + },
969 + "pluginVersion": "10.4.1",
970 + "targets": [
971 + {
972 + "bucketAggs": [
973 + {
974 + "fake": true,
975 + "field": "reason",
976 + "id": "6",
977 + "settings": {
978 + "min_doc_count": 1,
979 + "order": "desc",
980 + "orderBy": "_count",
981 + "size": "10"
982 + },
983 + "type": "terms"
984 + },
985 + {
986 + "fake": true,
987 + "field": "timestamp",
988 + "id": "5",
989 + "settings": {
990 + "interval": "auto",
991 + "min_doc_count": 0,
992 + "trimEdges": 0
993 + },
994 + "type": "date_histogram"
995 + }
996 + ],
997 + "datasource": {
998 + "type": "grafana-opensearch-datasource",
999 + "uid": "replace_datasource_uid"
1000 + },
1001 + "metrics": [
1002 + {
1003 + "field": "type",
1004 + "id": "1",
1005 + "meta": {},
1006 + "settings": {},
1007 + "type": "count"
1008 + }
1009 + ],
1010 + "query": "device_name:$device AND severity:$severity",
1011 + "refId": "A",
1012 + "timeField": "timestamp"
1013 + }
1014 + ],
1015 + "title": "TOP 10 DETECTIONS",
1016 + "type": "bargauge"
1017 + },
1018 + {
1019 + "datasource": {
1020 + "type": "grafana-opensearch-datasource",
1021 + "uid": "replace_datasource_uid"
1022 + },
1023 + "fieldConfig": {
1024 + "defaults": {
1025 + "mappings": [
1026 + {
1027 + "options": {
1028 + "match": "null",
1029 + "result": {
1030 + "text": "N/A"
1031 + }
1032 + },
1033 + "type": "special"
1034 + }
1035 + ],
1036 + "thresholds": {
1037 + "mode": "absolute",
1038 + "steps": [
1039 + {
1040 + "color": "orange",
1041 + "value": null
1042 + }
1043 + ]
1044 + },
1045 + "unit": "locale"
1046 + },
1047 + "overrides": []
1048 + },
1049 + "gridPos": {
1050 + "h": 6,
1051 + "w": 3,
1052 + "x": 0,
1053 + "y": 6
1054 + },
1055 + "id": 2,
1056 + "options": {
1057 + "colorMode": "value",
1058 + "graphMode": "area",
1059 + "justifyMode": "auto",
1060 + "orientation": "horizontal",
1061 + "reduceOptions": {
1062 + "calcs": [
1063 + "sum"
1064 + ],
1065 + "fields": "",
1066 + "values": false
1067 + },
1068 + "showPercentChange": false,
1069 + "text": {},
1070 + "textMode": "auto",
1071 + "wideLayout": true
1072 + },
1073 + "pluginVersion": "10.4.1",
1074 + "targets": [
1075 + {
1076 + "bucketAggs": [
1077 + {
1078 + "$$hashKey": "object:331",
1079 + "field": "timestamp",
1080 + "id": "2",
1081 + "settings": {
1082 + "interval": "auto",
1083 + "min_doc_count": 0,
1084 + "trimEdges": 0
1085 + },
1086 + "type": "date_histogram"
1087 + }
1088 + ],
1089 + "datasource": {
1090 + "type": "grafana-opensearch-datasource",
1091 + "uid": "replace_datasource_uid"
1092 + },
1093 + "metrics": [
1094 + {
1095 + "$$hashKey": "object:329",
1096 + "field": "select field",
1097 + "id": "1",
1098 + "type": "count"
1099 + }
1100 + ],
1101 + "query": "device_name:$device AND severity:$severity",
1102 + "refId": "A",
1103 + "timeField": "timestamp"
1104 + }
1105 + ],
1106 + "title": "EVENTS (TOTAL)",
1107 + "type": "stat"
1108 + },
1109 + {
1110 + "datasource": {
1111 + "type": "grafana-opensearch-datasource",
1112 + "uid": "replace_datasource_uid"
1113 + },
1114 + "fieldConfig": {
1115 + "defaults": {
1116 + "color": {
1117 + "mode": "thresholds"
1118 + },
1119 + "custom": {
1120 + "align": "auto",
1121 + "cellOptions": {
1122 + "type": "auto"
1123 + },
1124 + "inspect": false
1125 + },
1126 + "mappings": [],
1127 + "thresholds": {
1128 + "mode": "absolute",
1129 + "steps": [
1130 + {
1131 + "color": "green",
1132 + "value": null
1133 + },
1134 + {
1135 + "color": "red",
1136 + "value": 80
1137 + }
1138 + ]
1139 + }
1140 + },
1141 + "overrides": [
1142 + {
1143 + "matcher": {
1144 + "id": "byName",
1145 + "options": "process_image"
1146 + },
1147 + "properties": [
1148 + {
1149 + "id": "custom.width",
1150 + "value": 657
1151 + }
1152 + ]
1153 + }
1154 + ]
1155 + },
1156 + "gridPos": {
1157 + "h": 14,
1158 + "w": 6,
1159 + "x": 0,
1160 + "y": 12
1161 + },
1162 + "id": 10,
1163 + "options": {
1164 + "cellHeight": "sm",
1165 + "footer": {
1166 + "countRows": false,
1167 + "fields": "",
1168 + "reducer": [
1169 + "sum"
1170 + ],
1171 + "show": false
1172 + },
1173 + "showHeader": true,
1174 + "sortBy": []
1175 + },
1176 + "pluginVersion": "10.4.1",
1177 + "targets": [
1178 + {
1179 + "bucketAggs": [
1180 + {
1181 + "fake": true,
1182 + "field": "device_name",
1183 + "id": "6",
1184 + "settings": {
1185 + "min_doc_count": 1,
1186 + "order": "desc",
1187 + "orderBy": "_count",
1188 + "size": "0"
1189 + },
1190 + "type": "terms"
1191 + }
1192 + ],
1193 + "datasource": {
1194 + "type": "grafana-opensearch-datasource",
1195 + "uid": "replace_datasource_uid"
1196 + },
1197 + "metrics": [
1198 + {
1199 + "field": "type",
1200 + "id": "1",
1201 + "meta": {},
1202 + "settings": {},
1203 + "type": "count"
1204 + }
1205 + ],
1206 + "query": "device_name:$device AND severity:$severity",
1207 + "refId": "A",
1208 + "timeField": "timestamp"
1209 + }
1210 + ],
1211 + "title": "DEVICES",
1212 + "type": "table"
1213 + },
1214 + {
1215 + "datasource": {
1216 + "type": "grafana-opensearch-datasource",
1217 + "uid": "replace_datasource_uid"
1218 + },
1219 + "fieldConfig": {
1220 + "defaults": {
1221 + "color": {
1222 + "mode": "palette-classic"
1223 + },
1224 + "custom": {
1225 + "axisBorderShow": false,
1226 + "axisCenteredZero": false,
1227 + "axisColorMode": "text",
1228 + "axisLabel": "",
1229 + "axisPlacement": "auto",
1230 + "barAlignment": 0,
1231 + "drawStyle": "bars",
1232 + "fillOpacity": 0,
1233 + "gradientMode": "none",
1234 + "hideFrom": {
1235 + "legend": false,
1236 + "tooltip": false,
1237 + "viz": false
1238 + },
1239 + "insertNulls": false,
1240 + "lineInterpolation": "linear",
1241 + "lineWidth": 1,
1242 + "pointSize": 5,
1243 + "scaleDistribution": {
1244 + "type": "linear"
1245 + },
1246 + "showPoints": "auto",
1247 + "spanNulls": false,
1248 + "stacking": {
1249 + "group": "A",
1250 + "mode": "normal"
1251 + },
1252 + "thresholdsStyle": {
1253 + "mode": "off"
1254 + }
1255 + },
1256 + "mappings": [],
1257 + "thresholds": {
1258 + "mode": "absolute",
1259 + "steps": [
1260 + {
1261 + "color": "green",
1262 + "value": null
1263 + },
1264 + {
1265 + "color": "red",
1266 + "value": 80
1267 + }
1268 + ]
1269 + }
1270 + },
1271 + "overrides": []
1272 + },
1273 + "gridPos": {
1274 + "h": 14,
1275 + "w": 18,
1276 + "x": 6,
1277 + "y": 12
1278 + },
1279 + "id": 5,
1280 + "options": {
1281 + "legend": {
1282 + "calcs": [],
1283 + "displayMode": "table",
1284 + "placement": "right",
1285 + "showLegend": true
1286 + },
1287 + "tooltip": {
1288 + "mode": "single",
1289 + "sort": "none"
1290 + }
1291 + },
1292 + "targets": [
1293 + {
1294 + "alias": "",
1295 + "bucketAggs": [
1296 + {
1297 + "field": "device_name",
1298 + "id": "3",
1299 + "settings": {
1300 + "min_doc_count": "1",
1301 + "order": "desc",
1302 + "orderBy": "_count",
1303 + "size": "10"
1304 + },
1305 + "type": "terms"
1306 + },
1307 + {
1308 + "field": "timestamp",
1309 + "id": "2",
1310 + "settings": {
1311 + "interval": "5m"
1312 + },
1313 + "type": "date_histogram"
1314 + }
1315 + ],
1316 + "datasource": {
1317 + "type": "grafana-opensearch-datasource",
1318 + "uid": "replace_datasource_uid"
1319 + },
1320 + "metrics": [
1321 + {
1322 + "id": "1",
1323 + "type": "count"
1324 + }
1325 + ],
1326 + "query": "device_name:$device AND severity:$severity",
1327 + "refId": "A",
1328 + "timeField": "timestamp"
1329 + }
1330 + ],
1331 + "title": "EVENTS BY DEVICE (TOP 10) - HISTOGRAM",
1332 + "transparent": true,
1333 + "type": "timeseries"
1334 + },
1335 + {
1336 + "datasource": {
1337 + "type": "grafana-opensearch-datasource",
1338 + "uid": "replace_datasource_uid"
1339 + },
1340 + "fieldConfig": {
1341 + "defaults": {
1342 + "color": {
1343 + "mode": "thresholds"
1344 + },
1345 + "custom": {
1346 + "align": "auto",
1347 + "cellOptions": {
1348 + "type": "auto"
1349 + },
1350 + "inspect": false
1351 + },
1352 + "mappings": [],
1353 + "thresholds": {
1354 + "mode": "absolute",
1355 + "steps": [
1356 + {
1357 + "color": "green",
1358 + "value": null
1359 + },
1360 + {
1361 + "color": "red",
1362 + "value": 80
1363 + }
1364 + ]
1365 + }
1366 + },
1367 + "overrides": [
1368 + {
1369 + "matcher": {
1370 + "id": "byName",
1371 + "options": "process_image"
1372 + },
1373 + "properties": [
1374 + {
1375 + "id": "custom.width",
1376 + "value": 657
1377 + }
1378 + ]
1379 + }
1380 + ]
1381 + },
1382 + "gridPos": {
1383 + "h": 12,
1384 + "w": 6,
1385 + "x": 0,
1386 + "y": 26
1387 + },
1388 + "id": 11,
1389 + "options": {
1390 + "cellHeight": "sm",
1391 + "footer": {
1392 + "countRows": false,
1393 + "fields": "",
1394 + "reducer": [
1395 + "sum"
1396 + ],
1397 + "show": false
1398 + },
1399 + "showHeader": true,
1400 + "sortBy": []
1401 + },
1402 + "pluginVersion": "10.4.1",
1403 + "targets": [
1404 + {
1405 + "bucketAggs": [
1406 + {
1407 + "fake": true,
1408 + "field": "device_username",
1409 + "id": "6",
1410 + "settings": {
1411 + "min_doc_count": 1,
1412 + "order": "desc",
1413 + "orderBy": "_count",
1414 + "size": "0"
1415 + },
1416 + "type": "terms"
1417 + }
1418 + ],
1419 + "datasource": {
1420 + "type": "grafana-opensearch-datasource",
1421 + "uid": "replace_datasource_uid"
1422 + },
1423 + "metrics": [
1424 + {
1425 + "field": "type",
1426 + "id": "1",
1427 + "meta": {},
1428 + "settings": {},
1429 + "type": "count"
1430 + }
1431 + ],
1432 + "query": "device_name:$device AND severity:$severity",
1433 + "refId": "A",
1434 + "timeField": "timestamp"
1435 + }
1436 + ],
1437 + "title": "USER ACCOUNTS",
1438 + "transparent": true,
1439 + "type": "table"
1440 + },
1441 + {
1442 + "datasource": {
1443 + "type": "grafana-opensearch-datasource",
1444 + "uid": "replace_datasource_uid"
1445 + },
1446 + "fieldConfig": {
1447 + "defaults": {
1448 + "color": {
1449 + "mode": "palette-classic"
1450 + },
1451 + "custom": {
1452 + "axisBorderShow": false,
1453 + "axisCenteredZero": false,
1454 + "axisColorMode": "text",
1455 + "axisLabel": "",
1456 + "axisPlacement": "auto",
1457 + "barAlignment": 0,
1458 + "drawStyle": "bars",
1459 + "fillOpacity": 0,
1460 + "gradientMode": "none",
1461 + "hideFrom": {
1462 + "legend": false,
1463 + "tooltip": false,
1464 + "viz": false
1465 + },
1466 + "insertNulls": false,
1467 + "lineInterpolation": "linear",
1468 + "lineWidth": 1,
1469 + "pointSize": 5,
1470 + "scaleDistribution": {
1471 + "type": "linear"
1472 + },
1473 + "showPoints": "auto",
1474 + "spanNulls": false,
1475 + "stacking": {
1476 + "group": "A",
1477 + "mode": "normal"
1478 + },
1479 + "thresholdsStyle": {
1480 + "mode": "off"
1481 + }
1482 + },
1483 + "mappings": [],
1484 + "thresholds": {
1485 + "mode": "absolute",
1486 + "steps": [
1487 + {
1488 + "color": "green",
1489 + "value": null
1490 + },
1491 + {
1492 + "color": "red",
1493 + "value": 80
1494 + }
1495 + ]
1496 + }
1497 + },
1498 + "overrides": []
1499 + },
1500 + "gridPos": {
1501 + "h": 12,
1502 + "w": 18,
1503 + "x": 6,
1504 + "y": 26
1505 + },
1506 + "id": 4,
1507 + "options": {
1508 + "legend": {
1509 + "calcs": [],
1510 + "displayMode": "table",
1511 + "placement": "right",
1512 + "showLegend": true
1513 + },
1514 + "tooltip": {
1515 + "mode": "single",
1516 + "sort": "none"
1517 + }
1518 + },
1519 + "targets": [
1520 + {
1521 + "alias": "",
1522 + "bucketAggs": [
1523 + {
1524 + "field": "severity",
1525 + "id": "3",
1526 + "settings": {
1527 + "min_doc_count": "1",
1528 + "order": "desc",
1529 + "orderBy": "_count",
1530 + "size": "10"
1531 + },
1532 + "type": "terms"
1533 + },
1534 + {
1535 + "field": "timestamp",
1536 + "id": "2",
1537 + "settings": {
1538 + "interval": "5m"
1539 + },
1540 + "type": "date_histogram"
1541 + }
1542 + ],
1543 + "datasource": {
1544 + "type": "grafana-opensearch-datasource",
1545 + "uid": "replace_datasource_uid"
1546 + },
1547 + "metrics": [
1548 + {
1549 + "id": "1",
1550 + "type": "count"
1551 + }
1552 + ],
1553 + "query": "device_name:$device AND severity:$severity",
1554 + "refId": "A",
1555 + "timeField": "timestamp"
1556 + }
1557 + ],
1558 + "title": "EVENTS SEVERITY - HISTOGRAM",
1559 + "type": "timeseries"
1560 + },
1561 + {
1562 + "datasource": {
1563 + "type": "grafana-opensearch-datasource",
1564 + "uid": "replace_datasource_uid"
1565 + },
1566 + "fieldConfig": {
1567 + "defaults": {
1568 + "color": {
1569 + "mode": "thresholds"
1570 + },
1571 + "custom": {
1572 + "align": "auto",
1573 + "cellOptions": {
1574 + "type": "auto"
1575 + },
1576 + "filterable": true,
1577 + "inspect": false
1578 + },
1579 + "mappings": [],
1580 + "thresholds": {
1581 + "mode": "absolute",
1582 + "steps": [
1583 + {
1584 + "color": "green",
1585 + "value": null
1586 + },
1587 + {
1588 + "color": "red",
1589 + "value": 80
1590 + }
1591 + ]
1592 + }
1593 + },
1594 + "overrides": [
1595 + {
1596 + "matcher": {
1597 + "id": "byName",
1598 + "options": "EVENT ID"
1599 + },
1600 + "properties": [
1601 + {
1602 + "id": "links",
1603 + "value": [
1604 + {
1605 + "targetBlank": true,
1606 + "title": "EVENT DETAILS",
1607 + "url": "https://grafana.company.local/explore?left=%7B%22datasource%22:%22CARBONBLACK%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22query%22:%22_id:${__value.text}%22,%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D"
1608 + }
1609 + ]
1610 + }
1611 + ]
1612 + },
1613 + {
1614 + "matcher": {
1615 + "id": "byName",
1616 + "options": "MDR ALERT"
1617 + },
1618 + "properties": [
1619 + {
1620 + "id": "custom.width",
1621 + "value": 126
1622 + }
1623 + ]
1624 + },
1625 + {
1626 + "matcher": {
1627 + "id": "byName",
1628 + "options": "EXT IP COUNTRY"
1629 + },
1630 + "properties": [
1631 + {
1632 + "id": "custom.width",
1633 + "value": 156
1634 + }
1635 + ]
1636 + },
1637 + {
1638 + "matcher": {
1639 + "id": "byName",
1640 + "options": "DEVICE EXT IP"
1641 + },
1642 + "properties": [
1643 + {
1644 + "id": "custom.width",
1645 + "value": 159
1646 + }
1647 + ]
1648 + },
1649 + {
1650 + "matcher": {
1651 + "id": "byName",
1652 + "options": "EVENT TYPE"
1653 + },
1654 + "properties": [
1655 + {
1656 + "id": "custom.width",
1657 + "value": 152
1658 + }
1659 + ]
1660 + },
1661 + {
1662 + "matcher": {
1663 + "id": "byName",
1664 + "options": "MITRE"
1665 + },
1666 + "properties": [
1667 + {
1668 + "id": "custom.width",
1669 + "value": 110
1670 + }
1671 + ]
1672 + },
1673 + {
1674 + "matcher": {
1675 + "id": "byName",
1676 + "options": "SENSOR ACTION"
1677 + },
1678 + "properties": [
1679 + {
1680 + "id": "custom.width",
1681 + "value": 159
1682 + }
1683 + ]
1684 + },
1685 + {
1686 + "matcher": {
1687 + "id": "byName",
1688 + "options": "REASON"
1689 + },
1690 + "properties": [
1691 + {
1692 + "id": "custom.width",
1693 + "value": 403
1694 + }
1695 + ]
1696 + }
1697 + ]
1698 + },
1699 + "gridPos": {
1700 + "h": 17,
1701 + "w": 24,
1702 + "x": 0,
1703 + "y": 38
1704 + },
1705 + "id": 6,
1706 + "options": {
1707 + "cellHeight": "sm",
1708 + "footer": {
1709 + "countRows": false,
1710 + "enablePagination": true,
1711 + "fields": "",
1712 + "reducer": [
1713 + "sum"
1714 + ],
1715 + "show": false
1716 + },
1717 + "showHeader": true,
1718 + "sortBy": []
1719 + },
1720 + "pluginVersion": "10.4.1",
1721 + "targets": [
1722 + {
1723 + "alias": "",
1724 + "bucketAggs": [],
1725 + "datasource": {
1726 + "type": "grafana-opensearch-datasource",
1727 + "uid": "replace_datasource_uid"
1728 + },
1729 + "format": "table",
1730 + "metrics": [
1731 + {
1732 + "id": "1",
1733 + "settings": {
1734 + "order": "desc",
1735 + "size": "500",
1736 + "useTimeRange": true
1737 + },
1738 + "type": "raw_data"
1739 + }
1740 + ],
1741 + "query": "device_name:$device AND severity:$severity",
1742 + "queryType": "lucene",
1743 + "refId": "A",
1744 + "timeField": "timestamp"
1745 + }
1746 + ],
1747 + "title": "CB EVENTS",
1748 + "transformations": [
1749 + {
1750 + "id": "filterFieldsByName",
1751 + "options": {
1752 + "include": {
1753 + "names": [
1754 + "timestamp",
1755 + "_id",
1756 + "attack_tactic",
1757 + "customer_code",
1758 + "device_external_ip",
1759 + "device_external_ip_country_code",
1760 + "device_internal_ip",
1761 + "device_name",
1762 + "device_username",
1763 + "mdr_alert",
1764 + "reason",
1765 + "sensor_action",
1766 + "severity",
1767 + "type"
1768 + ]
1769 + }
1770 + }
1771 + },
1772 + {
1773 + "id": "organize",
1774 + "options": {
1775 + "excludeByName": {},
1776 + "includeByName": {},
1777 + "indexByName": {
1778 + "_id": 1,
1779 + "attack_tactic": 8,
1780 + "customer_code": 9,
1781 + "device_external_ip": 5,
1782 + "device_external_ip_country_code": 6,
1783 + "device_internal_ip": 7,
1784 + "device_name": 3,
1785 + "device_username": 4,
1786 + "mdr_alert": 10,
1787 + "reason": 11,
1788 + "sensor_action": 12,
1789 + "severity": 13,
1790 + "timestamp": 0,
1791 + "type": 2
1792 + },
1793 + "renameByName": {
1794 + "_id": "EVENT ID",
1795 + "attack_tactic": "MITRE",
1796 + "customer_code": "CUSTOMER",
1797 + "device_external_ip": "DEVICE EXT IP",
1798 + "device_external_ip_country_code": "EXT IP COUNTRY",
1799 + "device_internal_ip": "DEVICE INTERNAL IP",
1800 + "device_name": "DEVICE",
1801 + "device_username": "USER",
1802 + "mdr_alert": "MDR ALERT",
1803 + "reason": "REASON",
1804 + "sensor_action": "SENSOR ACTION",
1805 + "severity": "SEVERITY",
1806 + "timestamp": "DATE/TIME",
1807 + "type": "EVENT TYPE"
1808 + }
1809 + }
1810 + }
1811 + ],
1812 + "transparent": true,
1813 + "type": "table"
1814 + }
1815 + ],
1816 + "schemaVersion": 39,
1817 + "tags": [],
1818 + "templating": {
1819 + "list": [
1820 + {
1821 + "datasource": {
1822 + "type": "grafana-opensearch-datasource",
1823 + "uid": "replace_datasource_uid"
1824 + },
1825 + "filters": [],
1826 + "hide": 0,
1827 + "label": "Filters",
1828 + "name": "query0",
1829 + "skipUrlSync": false,
1830 + "type": "adhoc"
1831 + },
1832 + {
1833 + "current": {
1834 + "selected": false,
1835 + "text": "All",
1836 + "value": "$__all"
1837 + },
1838 + "datasource": {
1839 + "type": "grafana-opensearch-datasource",
1840 + "uid": "replace_datasource_uid"
1841 + },
1842 + "definition": "{ \"find\": \"terms\", \"field\": \"severity\", \"query\": \"\"}",
1843 + "hide": 0,
1844 + "includeAll": true,
1845 + "label": "Severity",
1846 + "multi": false,
1847 + "name": "severity",
1848 + "options": [],
1849 + "query": "{ \"find\": \"terms\", \"field\": \"severity\", \"query\": \"\"}",
1850 + "refresh": 1,
1851 + "regex": "",
1852 + "skipUrlSync": false,
1853 + "sort": 0,
1854 + "type": "query"
1855 + },
1856 + {
1857 + "current": {
1858 + "selected": false,
1859 + "text": "All",
1860 + "value": "$__all"
1861 + },
1862 + "datasource": {
1863 + "type": "grafana-opensearch-datasource",
1864 + "uid": "replace_datasource_uid"
1865 + },
1866 + "definition": "{ \"find\": \"terms\", \"field\": \"device_name\", \"query\": \"\"}",
1867 + "hide": 0,
1868 + "includeAll": true,
1869 + "label": "Device",
1870 + "multi": false,
1871 + "name": "device",
1872 + "options": [],
1873 + "query": "{ \"find\": \"terms\", \"field\": \"device_name\", \"query\": \"\"}",
1874 + "refresh": 1,
1875 + "regex": "",
1876 + "skipUrlSync": false,
1877 + "sort": 0,
1878 + "type": "query"
1879 + }
1880 + ]
1881 + },
1882 + "time": {
1883 + "from": "now-24h",
1884 + "to": "now"
1885 + },
1886 + "timepicker": {},
1887 + "timezone": "browser",
1888 + "title": "CARBON BLACK - _SUMMARY",
1889 + "version": 8,
1890 + "weekStart": ""
1891 +}
backend/app/connectors/grafana/routes/reporting.py
-3
@@ -6,7 +6,6 @@ from typing import List
6 from fastapi import APIRouter
7 from fastapi import Depends
8 from fastapi import Security
9 -from fastapi.exceptions import HTTPException
9 from loguru import logger
10 from sqlalchemy.ext.asyncio import AsyncSession
11 from sqlalchemy.future import select
@@ -221,7 +220,5 @@ async def generate_grafana_iframe_links(
220 )
221 async def create_report(request: GenerateReportRequest, session: AsyncSession = Depends(get_db)) -> GenerateReportResponse:
222 logger.info("Generating report")
224 - # ! License Check
223 # await is_feature_enabled("REPORTING", session)
226 - raise HTTPException(status_code=501, detail="Feature not enabled. Please check back later.")
224 return await generate_report(request, session)
backend/app/connectors/grafana/schema/dashboards.py
+5
@@ -75,6 +75,10 @@ class HuntressDashboard(Enum):
75 HUNTRESS_SUMMARY = ("Huntress", "summary.json")
76
77
78 +class CarbonBlackDashboard(Enum):
79 + CARBONBLACK_SUMMARY = ("CarbonBlack", "summary.json")
80 +
81 +
82 class DashboardProvisionRequest(BaseModel):
83 dashboards: List[str] = Field(
84 ...,
@@ -103,6 +107,7 @@ class DashboardProvisionRequest(BaseModel):
107 + list(MimecastDashboard)
108 + list(SapSiemDashboard)
109 + list(HuntressDashboard)
110 + + list(CarbonBlackDashboard)
111 }
112 if e not in valid_dashboards:
113 raise ValueError(f'Dashboard identifier "{e}" is not recognized.')
backend/app/connectors/grafana/services/dashboards.py
+2
@@ -4,6 +4,7 @@ from pathlib import Path
4 from fastapi import HTTPException
5 from loguru import logger
6
7 +from app.connectors.grafana.schema.dashboards import CarbonBlackDashboard
8 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
9 from app.connectors.grafana.schema.dashboards import GrafanaDashboard
10 from app.connectors.grafana.schema.dashboards import GrafanaDashboardResponse
@@ -175,6 +176,7 @@ async def provision_dashboards(
176 + list(MimecastDashboard)
177 + list(SapSiemDashboard)
178 + list(HuntressDashboard)
179 + + list(CarbonBlackDashboard)
180 }
181
182 for dashboard_name in dashboard_request.dashboards:
backend/app/connectors/shuffle/services/integrations.py
+1 -1
@@ -16,4 +16,4 @@ async def execute_integration(request: IntegrationRequest) -> dict:
16 """
17 logger.info(f"Executing integration: {request}")
18 response = await send_post_request("/api/v1/apps/categories/run", request.dict())
19 - logger.info(f"Integration executed: {response}")
19 + return response
backend/app/connectors/wazuh_indexer/schema/indices.py
+1
@@ -19,6 +19,7 @@ class IndexConfigModel(BaseModel):
19 ".opensearch": True,
20 ".kibana": True,
21 "praeco": True,
22 + "filebeat": True,
23 },
24 description="A dictionary containing index names to be skipped and their skip status.",
25 )
backend/app/connectors/wazuh_indexer/services/alerts.py
+7 -1
@@ -113,7 +113,13 @@ async def collect_alerts_generic(
113 if "No mapping found for [timestamp_utc] in order to sort on" in str(e):
114 logger.warning("Retrying with timestamp field set to 'timestamp'")
115 body.timestamp_field = "timestamp"
116 - return await collect_alerts_generic(index_name, body, is_host_specific)
116 + try:
117 + return await collect_alerts_generic(index_name, body, is_host_specific)
118 + except RequestError as e:
119 + if "No mapping found for [timestamp] in order to sort on" in str(e):
120 + logger.warning("Retrying with timestamp field set to '@timestamp'")
121 + body.timestamp_field = "@timestamp"
122 + return await collect_alerts_generic(index_name, body, is_host_specific)
123 else:
124 logger.warning(f"An error occurred while collecting alerts: {e}")
125 raise HTTPException(
backend/app/customer_provisioning/routes/provision.py
+2 -1
@@ -207,7 +207,8 @@ async def provision_customer_route(
207 Returns:
208 CustomerProvisionResponse: The response data for the provisioned customer.
209 """
210 - await check_unique_ports(request, session)
210 + if request.provision_wazuh_worker is True:
211 + await check_unique_ports(request, session)
212 logger.info("Provisioning new customer")
213 if request.only_insert_into_db is True:
214 logger.info("Only inserting into the database")
backend/app/customer_provisioning/schema/provision.py
+4
@@ -80,6 +80,10 @@ class ProvisionNewCustomer(BaseModel):
80 None,
81 description="ID of the DFIR Iris customer",
82 )
83 + dfir_iris_username: Optional[str] = Field(
84 + None,
85 + description="Username of the DFIR Iris customer",
86 + )
87 graylog_index_id: Optional[str] = Field(
88 None,
89 description="ID of the Graylog index set",
backend/app/customer_provisioning/services/dfir_iris.py
+23
@@ -1,11 +1,13 @@
1 from fastapi import HTTPException
2 from loguru import logger
3
4 +from app.connectors.dfir_iris.routes.users import add_user_to_customers_route
5 from app.connectors.dfir_iris.schema.admin import CreateCustomerResponse
6 from app.connectors.dfir_iris.schema.admin import ListCustomers
7 from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
8 from app.connectors.dfir_iris.utils.universal import initialize_client_and_admin
9 from app.connectors.dfir_iris.utils.universal import initialize_client_and_customer
10 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_user
11
12
13 async def check_customer_exists(customer_name: str) -> bool:
@@ -48,6 +50,27 @@ async def create_customer(customer_name: str) -> CreateCustomerResponse:
50 return CreateCustomerResponse(success=result["success"], data=result["data"])
51
52
53 +async def add_user_to_all_customers(username: str):
54 + """
55 + Add a user to all customers.
56 +
57 + Args:
58 + username (str): The username of the user to be added.
59 +
60 + Returns:
61 + None
62 + """
63 + client, user = await initialize_client_and_user("DFIR-IRIS")
64 + user = await fetch_and_validate_data(client, user.get_user, username)
65 + if user is None or not user["success"]:
66 + raise HTTPException(
67 + status_code=400,
68 + detail=f"User {username} does not exist",
69 + )
70 + logger.info(f"User: {user}")
71 + await add_user_to_customers_route(user["data"]["user_id"])
72 +
73 +
74 async def delete_customer(customer_id: int):
75 """
76 Deletes a customer with the given customer_id.
backend/app/customer_provisioning/services/provision.py
+6
@@ -13,6 +13,7 @@ from app.customer_provisioning.schema.provision import ProvisionHaProxyRequest
13 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
14 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerRequest
15 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerResponse
16 +from app.customer_provisioning.services.dfir_iris import add_user_to_all_customers
17 from app.customer_provisioning.services.dfir_iris import create_customer
18 from app.customer_provisioning.services.grafana import create_grafana_datasource
19 from app.customer_provisioning.services.grafana import create_grafana_folder
@@ -96,6 +97,11 @@ async def provision_wazuh_customer(
97 except Exception:
98 provision_meta_data["iris_customer_id"] = 2
99
100 + if request.dfir_iris_username is not None:
101 + await add_user_to_all_customers(
102 + request.dfir_iris_username,
103 + )
104 +
105 customer_provision_meta = CustomerProvisionMeta(**provision_meta_data)
106 customer_meta = await update_customer_meta_table(
107 request,
backend/app/db/db_populate.py
+5
@@ -266,6 +266,7 @@ def get_available_integrations_list():
266 ("Mimecast", "Integrate Mimecast with SOCFortress."),
267 ("SAP SIEM", "Integrate SAP SIEM with SOCFortress."),
268 ("Huntress", "Integrate Huntress with SOCFortress."),
269 + ("CarbonBlack", "Integrate CarbonBlack with SOCFortress."),
270 # ... Add more available integrations as needed ...
271 ]
272
@@ -361,6 +362,10 @@ async def get_available_integrations_auth_keys_list(session: AsyncSession):
362 ("SAP SIEM", "API_DOMAIN"),
363 ("Huntress", "API_KEY"),
364 ("Huntress", "API_SECRET"),
365 + ("CarbonBlack", "API_KEY"),
366 + ("CarbonBlack", "API_URL"),
367 + ("CarbonBlack", "API_ID"),
368 + ("CarbonBlack", "ORGANIZATION_KEY"),
369 # ... Add more available integrations auth keys as needed ...
370 ]
371
backend/app/integrations/carbonblack/routes/provision.py new
+70
@@ -0,0 +1,70 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from sqlalchemy.ext.asyncio import AsyncSession
4 +
5 +from app.db.db_session import get_db
6 +from app.integrations.carbonblack.schema.provision import ProvisionCarbonBlackRequest
7 +from app.integrations.carbonblack.schema.provision import ProvisionCarbonBlackResponse
8 +from app.integrations.carbonblack.services.provision import provision_carbonblack
9 +from app.integrations.utils.utils import get_customer_integration_response
10 +from app.schedulers.models.scheduler import CreateSchedulerRequest
11 +from app.schedulers.scheduler import add_scheduler_jobs
12 +from app.schedulers.services.invoke_carbonblack import (
13 + invoke_carbonblack_integration_collect,
14 +)
15 +
16 +integration_carbonblack_provision_scheduler_router = APIRouter()
17 +
18 +
19 +@integration_carbonblack_provision_scheduler_router.post(
20 + "/provision",
21 + response_model=ProvisionCarbonBlackResponse,
22 + description="Provision a CarbonBlack integration.",
23 +)
24 +async def provision_carbonblack_route(
25 + provision_carbonblack_request: ProvisionCarbonBlackRequest,
26 + session: AsyncSession = Depends(get_db),
27 +) -> ProvisionCarbonBlackResponse:
28 + """
29 + Provisions a carbonblack integration.
30 +
31 + Args:
32 + provision_carbonblack_request (ProvisionCarbonBlackRequest): The request object containing the necessary data for provisioning.
33 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
34 +
35 + Returns:
36 + ProvisionCarbonBlackResponse: The response object indicating the success or failure of the provisioning process.
37 + """
38 + # Check if the customer integration settings are available and can be provisioned
39 + await get_customer_integration_response(
40 + provision_carbonblack_request.customer_code,
41 + session,
42 + )
43 + await provision_carbonblack(provision_carbonblack_request, session)
44 + await add_scheduler_jobs(
45 + CreateSchedulerRequest(
46 + function_name="invoke_carbonblack_integration_collection",
47 + time_interval=provision_carbonblack_request.time_interval,
48 + job_id="invoke_carbonblack_integration_collection",
49 + ),
50 + )
51 + return ProvisionCarbonBlackResponse(
52 + success=True,
53 + message="CarbonBlack provisioned successfully",
54 + )
55 +
56 +
57 +@integration_carbonblack_provision_scheduler_router.get(
58 + "/test",
59 + response_model=ProvisionCarbonBlackResponse,
60 + description="Invoke a CarbonBlack integration for testing",
61 +)
62 +async def test() -> ProvisionCarbonBlackResponse:
63 + """
64 + Invoke a CarbonBlack integration for testing.
65 + """
66 + await invoke_carbonblack_integration_collect()
67 + return ProvisionCarbonBlackResponse(
68 + success=True,
69 + message="CarbonBlack provisioned successfully",
70 + )
backend/app/integrations/carbonblack/schema/provision.py new
+87
@@ -0,0 +1,87 @@
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from pydantic import BaseModel
7 +from pydantic import Field
8 +from pydantic import root_validator
9 +
10 +
11 +class ProvisionCarbonBlackRequest(BaseModel):
12 + customer_code: str = Field(
13 + ...,
14 + description="The customer code.",
15 + examples=["00002"],
16 + )
17 + time_interval: int = Field(
18 + ...,
19 + description="The time interval for the scheduler.",
20 + examples=[5],
21 + )
22 + integration_name: str = Field(
23 + "CarbonBlack",
24 + description="The integration name.",
25 + examples=["CarbonBlack"],
26 + )
27 +
28 + # ensure the `integration_name` is always set to "Mimecast"
29 + @root_validator(pre=True)
30 + def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
31 + values["integration_name"] = "CarbonBlack"
32 + return values
33 +
34 +
35 +class ProvisionCarbonBlackResponse(BaseModel):
36 + success: bool
37 + message: str
38 +
39 +
40 +# ! STREAMS ! #
41 +class StreamRule(BaseModel):
42 + field: str
43 + type: int
44 + inverted: bool
45 + value: str
46 +
47 +
48 +class CarbonBlackEventStream(BaseModel):
49 + title: str = Field(..., description="Title of the stream")
50 + description: str = Field(..., description="Description of the stream")
51 + index_set_id: str = Field(..., description="ID of the associated index set")
52 + rules: List[StreamRule] = Field(..., description="List of rules for the stream")
53 + matching_type: str = Field(..., description="Matching type for the rules")
54 + remove_matches_from_default_stream: bool = Field(
55 + ...,
56 + description="Whether to remove matches from the default stream",
57 + )
58 + content_pack: Optional[str] = Field(
59 + None,
60 + description="Associated content pack, if any",
61 + )
62 +
63 + class Config:
64 + schema_extra = {
65 + "example": {
66 + "title": "CarbonBlack SIEM EVENTS - Example Company",
67 + "description": "CarbonBlack SIEM EVENTS - Example Company",
68 + "index_set_id": "12345",
69 + "rules": [
70 + {
71 + "field": "customer_code",
72 + "type": 1,
73 + "inverted": False,
74 + "value": "ExampleCode",
75 + },
76 + {
77 + "field": "integration",
78 + "type": 1,
79 + "inverted": False,
80 + "value": "huntress",
81 + },
82 + ],
83 + "matching_type": "AND",
84 + "remove_matches_from_default_stream": True,
85 + "content_pack": None,
86 + },
87 + }
backend/app/integrations/carbonblack/services/provision.py new
+394
@@ -0,0 +1,394 @@
1 +import json
2 +from datetime import datetime
3 +
4 +from loguru import logger
5 +from sqlalchemy import and_
6 +from sqlalchemy import update
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +
9 +from app.connectors.grafana.schema.dashboards import CarbonBlackDashboard
10 +from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
11 +from app.connectors.grafana.services.dashboards import provision_dashboards
12 +from app.connectors.grafana.utils.universal import create_grafana_client
13 +from app.connectors.graylog.services.management import start_stream
14 +from app.connectors.graylog.utils.universal import send_post_request
15 +from app.customer_provisioning.schema.grafana import GrafanaDatasource
16 +from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse
17 +from app.customer_provisioning.schema.graylog import GraylogIndexSetCreationResponse
18 +from app.customer_provisioning.schema.graylog import StreamCreationResponse
19 +from app.customer_provisioning.schema.graylog import TimeBasedIndexSet
20 +from app.customer_provisioning.services.grafana import create_grafana_folder
21 +from app.customer_provisioning.services.grafana import get_opensearch_version
22 +from app.customers.routes.customers import get_customer
23 +from app.customers.routes.customers import get_customer_meta
24 +from app.integrations.carbonblack.schema.provision import CarbonBlackEventStream
25 +from app.integrations.carbonblack.schema.provision import ProvisionCarbonBlackRequest
26 +from app.integrations.carbonblack.schema.provision import ProvisionCarbonBlackResponse
27 +from app.integrations.models.customer_integration_settings import CustomerIntegrations
28 +from app.integrations.routes import create_integration_meta
29 +from app.integrations.schema import CustomerIntegrationsMetaSchema
30 +from app.utils import get_connector_attribute
31 +
32 +
33 +################## ! GRAYLOG ! ##################
34 +async def build_index_set_config(
35 + customer_code: str,
36 + session: AsyncSession,
37 +) -> TimeBasedIndexSet:
38 + """
39 + Build the configuration for a time-based index set.
40 +
41 + Args:
42 + request (ProvisionNewCustomer): The request object containing customer information.
43 +
44 + Returns:
45 + TimeBasedIndexSet: The configured time-based index set.
46 + """
47 + return TimeBasedIndexSet(
48 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - CARBONBLACK",
49 + description=f"{customer_code} - CARBONBLACK",
50 + index_prefix=f"carbonblack-{customer_code}",
51 + rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
52 + rotation_strategy={
53 + "type": "org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategyConfig",
54 + "rotation_period": "P1D",
55 + "rotate_empty_index_set": False,
56 + "max_rotation_period": None,
57 + },
58 + retention_strategy_class="org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy",
59 + retention_strategy={
60 + "type": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig",
61 + "max_number_of_indices": 30,
62 + },
63 + creation_date=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
64 + index_analyzer="standard",
65 + shards=1,
66 + replicas=0,
67 + index_optimization_max_num_segments=1,
68 + index_optimization_disabled=False,
69 + writable=True,
70 + field_type_refresh_interval=5000,
71 + )
72 +
73 +
74 +# Function to send the POST request and handle the response
75 +async def send_index_set_creation_request(
76 + index_set: TimeBasedIndexSet,
77 +) -> GraylogIndexSetCreationResponse:
78 + """
79 + Sends a request to create an index set in Graylog.
80 +
81 + Args:
82 + index_set (TimeBasedIndexSet): The index set to be created.
83 +
84 + Returns:
85 + GraylogIndexSetCreationResponse: The response from Graylog after creating the index set.
86 + """
87 + json_index_set = json.dumps(index_set.dict())
88 + logger.info(f"json_index_set set: {json_index_set}")
89 + response_json = await send_post_request(
90 + endpoint="/api/system/indices/index_sets",
91 + data=index_set.dict(),
92 + )
93 + return GraylogIndexSetCreationResponse(**response_json)
94 +
95 +
96 +async def create_index_set(
97 + customer_code: str,
98 + session: AsyncSession,
99 +) -> GraylogIndexSetCreationResponse:
100 + """
101 + Creates an index set for a new customer.
102 +
103 + Args:
104 + request (ProvisionNewCustomer): The request object containing the customer information.
105 +
106 + Returns:
107 + GraylogIndexSetCreationResponse: The response object containing the result of the index set creation.
108 + """
109 + logger.info(f"Creating index set for customer {customer_code}")
110 + index_set_config = await build_index_set_config(customer_code, session)
111 + return await send_index_set_creation_request(index_set_config)
112 +
113 +
114 +# ! Event STREAMS ! #
115 +# Function to create event stream configuration
116 +async def build_event_stream_config(
117 + customer_code: str,
118 + index_set_id: str,
119 + session: AsyncSession,
120 +) -> CarbonBlackEventStream:
121 + """
122 + Builds the configuration for the Huntress event stream.
123 +
124 + Args:
125 + customer_code (str): The customer code.
126 + index_set_id (str): The index set ID.
127 + session (AsyncSession): The async session.
128 +
129 + Returns:
130 + CarbonBlackEventStream: The configured Huntress event stream.
131 + """
132 + return CarbonBlackEventStream(
133 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - CARBONBLACK",
134 + description=f"{(await get_customer(customer_code, session)).customer.customer_name} - CARBONBLACK",
135 + index_set_id=index_set_id,
136 + rules=[
137 + {
138 + "field": "integration",
139 + "type": 1,
140 + "inverted": False,
141 + "value": "carbonblack",
142 + },
143 + {
144 + "field": "customer_code",
145 + "type": 1,
146 + "inverted": False,
147 + "value": f"{customer_code}",
148 + },
149 + ],
150 + matching_type="AND",
151 + remove_matches_from_default_stream=True,
152 + content_pack=None,
153 + )
154 +
155 +
156 +async def send_event_stream_creation_request(
157 + event_stream: CarbonBlackEventStream,
158 +) -> StreamCreationResponse:
159 + """
160 + Sends a request to create an event stream.
161 +
162 + Args:
163 + event_stream (SapSiemEventStream): The event stream to be created.
164 +
165 + Returns:
166 + StreamCreationResponse: The response containing the created event stream.
167 + """
168 + json_event_stream = json.dumps(event_stream.dict())
169 + logger.info(f"json_event_stream set: {json_event_stream}")
170 + response_json = await send_post_request(
171 + endpoint="/api/streams",
172 + data=event_stream.dict(),
173 + )
174 + return StreamCreationResponse(**response_json)
175 +
176 +
177 +async def create_event_stream(
178 + customer_code: str,
179 + index_set_id: str,
180 + session: AsyncSession,
181 +) -> StreamCreationResponse:
182 + """
183 + Creates an event stream for a customer.
184 +
185 + Args:
186 + request (ProvisionNewCustomer): The request object containing customer information.
187 + index_set_id (str): The ID of the index set.
188 +
189 + Returns:
190 + The result of the event stream creation request.
191 + """
192 + event_stream_config = await build_event_stream_config(
193 + customer_code,
194 + index_set_id,
195 + session,
196 + )
197 + return await send_event_stream_creation_request(event_stream_config)
198 +
199 +
200 +#### ! GRAFANA ! ####
201 +async def create_grafana_datasource(
202 + customer_code: str,
203 + session: AsyncSession,
204 +) -> GrafanaDataSourceCreationResponse:
205 + """
206 + Creates a Grafana datasource for the specified customer.
207 +
208 + Args:
209 + customer_code (str): The customer code.
210 + session (AsyncSession): The async session.
211 +
212 + Returns:
213 + GrafanaDataSourceCreationResponse: The response containing the created datasource details.
214 + """
215 + logger.info("Creating Grafana datasource")
216 + grafana_client = await create_grafana_client("Grafana")
217 + # Switch to the newly created organization
218 + grafana_client.user.switch_actual_user_organisation(
219 + (await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
220 + )
221 + datasource_payload = GrafanaDatasource(
222 + name="CARBONBLACK",
223 + type="grafana-opensearch-datasource",
224 + typeName="OpenSearch",
225 + access="proxy",
226 + url=await get_connector_attribute(
227 + connector_id=1,
228 + column_name="connector_url",
229 + session=session,
230 + ),
231 + database=f"carbonblack-{customer_code}*",
232 + basicAuth=True,
233 + basicAuthUser=await get_connector_attribute(
234 + connector_id=1,
235 + column_name="connector_username",
236 + session=session,
237 + ),
238 + secureJsonData={
239 + "basicAuthPassword": await get_connector_attribute(
240 + connector_id=1,
241 + column_name="connector_password",
242 + session=session,
243 + ),
244 + },
245 + isDefault=False,
246 + jsonData={
247 + "database": f"carbonblack-{customer_code}*",
248 + "flavor": "opensearch",
249 + "includeFrozen": False,
250 + "logLevelField": "severity",
251 + "logMessageField": "summary",
252 + "maxConcurrentShardRequests": 5,
253 + "pplEnabled": True,
254 + "timeField": "timestamp",
255 + "tlsSkipVerify": True,
256 + "version": await get_opensearch_version(),
257 + },
258 + readOnly=True,
259 + )
260 + results = grafana_client.datasource.create_datasource(
261 + datasource=datasource_payload.dict(),
262 + )
263 + return GrafanaDataSourceCreationResponse(**results)
264 +
265 +
266 +async def provision_carbonblack(
267 + provision_carbonblack_request: ProvisionCarbonBlackRequest,
268 + session: AsyncSession,
269 +) -> ProvisionCarbonBlackResponse:
270 + logger.info(
271 + f"Provisioning CarbonBlack integration for customer {provision_carbonblack_request.customer_code}.",
272 + )
273 +
274 + # Create Index Set
275 + index_set_id = (
276 + await create_index_set(
277 + customer_code=provision_carbonblack_request.customer_code,
278 + session=session,
279 + )
280 + ).data.id
281 + logger.info(f"Index set: {index_set_id}")
282 + # Create event stream
283 + stream_id = (
284 + await create_event_stream(
285 + provision_carbonblack_request.customer_code,
286 + index_set_id,
287 + session,
288 + )
289 + ).data.stream_id
290 + # Start stream
291 + await start_stream(stream_id=stream_id)
292 +
293 + # Grafana Deployment
294 + carbonblack_datasource_uid = (
295 + await create_grafana_datasource(
296 + customer_code=provision_carbonblack_request.customer_code,
297 + session=session,
298 + )
299 + ).datasource.uid
300 + grafana_carbonblack_folder_id = (
301 + await create_grafana_folder(
302 + organization_id=(
303 + await get_customer_meta(
304 + provision_carbonblack_request.customer_code,
305 + session,
306 + )
307 + ).customer_meta.customer_meta_grafana_org_id,
308 + folder_title="CARBONBLACK",
309 + )
310 + ).id
311 + await provision_dashboards(
312 + DashboardProvisionRequest(
313 + dashboards=[dashboard.name for dashboard in CarbonBlackDashboard],
314 + organizationId=(
315 + await get_customer_meta(
316 + provision_carbonblack_request.customer_code,
317 + session,
318 + )
319 + ).customer_meta.customer_meta_grafana_org_id,
320 + folderId=grafana_carbonblack_folder_id,
321 + datasourceUid=carbonblack_datasource_uid,
322 + ),
323 + )
324 + await create_integration_meta_entry(
325 + CustomerIntegrationsMetaSchema(
326 + customer_code=provision_carbonblack_request.customer_code,
327 + integration_name="CarbonBlack",
328 + graylog_input_id=None,
329 + graylog_index_id=index_set_id,
330 + graylog_stream_id=stream_id,
331 + grafana_org_id=(
332 + await get_customer_meta(
333 + provision_carbonblack_request.customer_code,
334 + session,
335 + )
336 + ).customer_meta.customer_meta_grafana_org_id,
337 + grafana_dashboard_folder_id=grafana_carbonblack_folder_id,
338 + ),
339 + session,
340 + )
341 + await update_customer_integration_table(
342 + provision_carbonblack_request.customer_code,
343 + session,
344 + )
345 +
346 + return ProvisionCarbonBlackResponse(
347 + success=True,
348 + message="CarbonBlack integration provisioned successfully.",
349 + )
350 +
351 +
352 +############## ! WRITE TO DB ! ##############
353 +async def create_integration_meta_entry(
354 + customer_integration_meta: CustomerIntegrationsMetaSchema,
355 + session: AsyncSession,
356 +) -> None:
357 + """
358 + Creates an entry for the customer integration meta in the database.
359 +
360 + Args:
361 + customer_integration_meta (CustomerIntegrationsMetaSchema): The customer integration meta object.
362 + session (AsyncSession): The async session object for database operations.
363 + """
364 + await create_integration_meta(customer_integration_meta, session)
365 + logger.info(
366 + f"Integration meta entry created for customer {customer_integration_meta.customer_code}.",
367 + )
368 +
369 +
370 +async def update_customer_integration_table(
371 + customer_code: str,
372 + session: AsyncSession,
373 +) -> None:
374 + """
375 + Updates the `customer_integrations` table to set the `deployed` column to True where the `customer_code`
376 + matches the given customer code and the `integration_service_name` is "Huntress".
377 +
378 + Args:
379 + customer_code (str): The customer code.
380 + session (AsyncSession): The async session object for making HTTP requests.
381 + """
382 + await session.execute(
383 + update(CustomerIntegrations)
384 + .where(
385 + and_(
386 + CustomerIntegrations.customer_code == customer_code,
387 + CustomerIntegrations.integration_service_name == "CarbonBlack",
388 + ),
389 + )
390 + .values(deployed=True),
391 + )
392 + await session.commit()
393 +
394 + return None
backend/app/integrations/huntress/routes/huntress.py deleted
-50
@@ -1,50 +0,0 @@
1 -from fastapi import APIRouter
2 -from fastapi import Depends
3 -from sqlalchemy.ext.asyncio import AsyncSession
4 -
5 -from app.db.db_session import get_db
6 -from app.integrations.huntress.schema.huntress import CollectHuntressRequest
7 -from app.integrations.huntress.schema.huntress import HuntressAuthKeys
8 -from app.integrations.huntress.schema.huntress import InvokeHuntressRequest
9 -from app.integrations.huntress.schema.huntress import InvokeHuntressResponse
10 -from app.integrations.huntress.services.collect import collect_huntress
11 -from app.integrations.routes import find_customer_integration
12 -from app.integrations.utils.utils import extract_auth_keys
13 -from app.integrations.utils.utils import get_customer_integration_response
14 -
15 -integration_huntress_router = APIRouter()
16 -
17 -
18 -@integration_huntress_router.post(
19 - "",
20 - response_model=InvokeHuntressResponse,
21 - description="Pull down Huntress Events.",
22 -)
23 -async def collect_huntress_route(huntress_request: InvokeHuntressRequest, session: AsyncSession = Depends(get_db)):
24 - """Pull down Huntress Events."""
25 - customer_integration_response = await get_customer_integration_response(
26 - huntress_request.customer_code,
27 - session,
28 - )
29 -
30 - customer_integration = await find_customer_integration(
31 - huntress_request.customer_code,
32 - huntress_request.integration_name,
33 - customer_integration_response,
34 - )
35 -
36 - huntress_auth_keys = extract_auth_keys(customer_integration, service_name="Huntress")
37 -
38 - auth_keys = HuntressAuthKeys(**huntress_auth_keys)
39 -
40 - await collect_huntress(
41 - request=(
42 - CollectHuntressRequest(
43 - customer_code=huntress_request.customer_code,
44 - apiKey=auth_keys.API_KEY,
45 - secretKey=auth_keys.API_SECRET,
46 - )
47 - ),
48 - )
49 -
50 - return InvokeHuntressResponse(success=True, message="Huntress Events collected successfully.")
backend/app/integrations/huntress/schema/huntress.py deleted
-139
@@ -1,139 +0,0 @@
1 -from typing import Dict
2 -from typing import List
3 -from typing import Optional
4 -
5 -from pydantic import BaseModel
6 -from pydantic import Field
7 -
8 -
9 -class InvokeHuntressRequest(BaseModel):
10 - customer_code: str = Field(
11 - ...,
12 - description="The customer code.",
13 - examples=["00002"],
14 - )
15 - integration_name: str = Field(
16 - "Huntress",
17 - description="The integration name.",
18 - examples=["Huntress"],
19 - )
20 -
21 -
22 -class InvokeHuntressResponse(BaseModel):
23 - success: bool = Field(
24 - ...,
25 - description="The success status.",
26 - examples=[True],
27 - )
28 - message: str = Field(
29 - ...,
30 - description="The message.",
31 - examples=["Huntress Events collected successfully."],
32 - )
33 -
34 -
35 -class HuntressAuthKeys(BaseModel):
36 - API_KEY: str = Field(
37 - ...,
38 - description="The API key.",
39 - examples=["123456"],
40 - )
41 - API_SECRET: str = Field(
42 - ...,
43 - description="The secret key.",
44 - examples=["123456"],
45 - )
46 -
47 -
48 -class CollectHuntressRequest(BaseModel):
49 - customer_code: str = Field(
50 - ...,
51 - description="The customer code.",
52 - examples=["00002"],
53 - )
54 - apiKey: str = Field(
55 - ...,
56 - description="The API key.",
57 - examples=["123456"],
58 - )
59 - secretKey: str = Field(
60 - ...,
61 - description="The secret key.",
62 - examples=["123456"],
63 - )
64 -
65 -
66 -class Remediation(BaseModel):
67 - id: int
68 - type: str
69 - status: str
70 - details: dict
71 - completable_by_task_response: bool
72 - completable_manually: bool
73 - display_action: str
74 - approved_at: Optional[str]
75 - approved_by: Optional[dict]
76 - completed_at: Optional[str]
77 -
78 -
79 -class IndicatorCount(BaseModel):
80 - footholds: Optional[int] = Field(0, description="The number of footholds.")
81 - monitored_files: int = 0
82 - process_detections: int = 0
83 - ransomware_canaries: int = 0
84 - antivirus_detections: int = 0
85 -
86 -
87 -class ApprovedBy(BaseModel):
88 - id: int
89 - email: str
90 - first_name: str
91 - last_name: str
92 -
93 -
94 -class Foothold(BaseModel):
95 - id: Optional[int] # May not be present in all responses
96 - display_name: str
97 - service_name: str
98 - command: str
99 - file_path: str
100 - virus_total_detections: str
101 - virus_total_url: str
102 -
103 -
104 -class IncidentReport(BaseModel):
105 - id: int
106 - status: str
107 - summary: Optional[str]
108 - body: str
109 - updated_at: str
110 - agent_id: Optional[int]
111 - platform: str
112 - status_updated_at: str
113 - organization_id: Optional[int]
114 - sent_at: str
115 - account_id: int
116 - subject: str
117 - remediations: List[Remediation]
118 - footholds: Optional[str] = Field(None, description="The footholds.")
119 - severity: str
120 - closed_at: Optional[str] = Field(None, description="The date the incident was closed.")
121 - indicator_types: List[str]
122 - indicator_counts: IndicatorCount
123 -
124 - def to_dict(self) -> Dict:
125 - return self.dict()
126 -
127 -
128 -class Pagination(BaseModel):
129 - current_page: int
130 - current_page_count: int
131 - limit: int
132 - total_count: int
133 - next_page: Optional[int] = Field(None, description="The next page.")
134 - next_page_url: Optional[str] = Field(None, description="The next page URL.")
135 -
136 -
137 -class HuntressIncidentResponse(BaseModel):
138 - incident_reports: List[IncidentReport]
139 - pagination: Pagination
backend/app/integrations/huntress/services/collect.py deleted
-86
@@ -1,86 +0,0 @@
1 -import base64
2 -from typing import List
3 -
4 -import httpx
5 -from loguru import logger
6 -
7 -from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
8 -from app.integrations.huntress.schema.huntress import CollectHuntressRequest
9 -from app.integrations.huntress.schema.huntress import HuntressIncidentResponse
10 -from app.integrations.huntress.schema.huntress import IncidentReport
11 -from app.integrations.utils.event_shipper import event_shipper
12 -from app.integrations.utils.schema import EventShipperPayload
13 -
14 -
15 -async def base64_encode(payload: CollectHuntressRequest) -> str:
16 - """Base64 encode the payload."""
17 - payload = f"{payload.apiKey}:{payload.secretKey}"
18 - payload_bytes = payload.encode("utf-8")
19 - base64_bytes = base64.b64encode(payload_bytes)
20 - base64_string = base64_bytes.decode("utf-8")
21 - return base64_string
22 -
23 -
24 -async def make_request(url: str, auth: str) -> HuntressIncidentResponse:
25 - headers = {"Accept": "application/json", "Authorization": f"Basic {auth}"}
26 - async with httpx.AsyncClient() as client:
27 - response = await client.get(url, headers=headers)
28 - return HuntressIncidentResponse(**response.json())
29 -
30 -
31 -async def get_next_page(response: HuntressIncidentResponse) -> str:
32 - if response.pagination and response.pagination.next_page_url:
33 - return response.pagination.next_page_url
34 - return None
35 -
36 -
37 -async def check_if_incident_exists(incident: IncidentReport) -> bool:
38 - """Check if the incident exists in the Wazuh-Indexer."""
39 - es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
40 - results = es_client.search(
41 - index="huntress*",
42 - body={
43 - "size": 1000,
44 - "query": {"bool": {"must": [{"term": {"id": incident.id}}]}},
45 - },
46 - )
47 - if results["hits"]["total"]["value"] > 0:
48 - logger.info("Event already exists in Wazuh-Indexer...Skipping")
49 - return True
50 - return False
51 -
52 -
53 -async def send_to_event_shipper(incident: IncidentReport, customer_code: str) -> None:
54 - exists = await check_if_incident_exists(incident)
55 - if not exists:
56 - message = EventShipperPayload(
57 - customer_code=customer_code,
58 - integration="huntress",
59 - version="1.0",
60 - **incident.to_dict(),
61 - )
62 - await event_shipper(message)
63 - return None
64 -
65 -
66 -async def process_incidents(incidents: List[IncidentReport], customer_code: str) -> None:
67 - """Process a list of incidents."""
68 - for incident in incidents:
69 - await send_to_event_shipper(incident, customer_code)
70 -
71 -
72 -async def process_pages(url: str, auth: str, customer_code: str) -> None:
73 - """Process all pages of incidents."""
74 - while url is not None:
75 - response = await make_request(url, auth)
76 - await process_incidents(response.incident_reports, customer_code)
77 - url = await get_next_page(response)
78 -
79 -
80 -async def collect_huntress(request: CollectHuntressRequest) -> None:
81 - """Pull down Huntress Events."""
82 - logger.info(f"Collecting Huntress Events with request: {request}")
83 - base64_string = await base64_encode(request)
84 - logger.info(f"Base64 encoded string: {base64_string}")
85 - url = "https://api.huntress.io/v1/incident_reports?page=1&limit=100"
86 - await process_pages(url, base64_string, request.customer_code)
backend/app/integrations/markdown/carbonblack.md new
+44
@@ -0,0 +1,44 @@
1 +# [CarbonBlack Integration](https://developer.carbonblack.com/reference/carbon-black-cloud/platform/latest/alerts-api/)
2 +
3 +CarbonBlack is a cloud-based endpoint security platform that provides continuous monitoring and threat detection for endpoints. This integration allows you to ingest CarbonBlack alerts into the SOCFortress SIEM stack.
4 +
5 +## Introduction
6 +
7 +The CarbonBlack API follows a RESTful pattern. Requests are made via resource-oriented URLs as described in this document and API responses are formatted as JSON data and ingested into the SOCFortress SIEM stack.
8 +
9 +## Use Cases
10 +
11 +- Poll for Alerts to ingest into a SIEM
12 +
13 +## Requirements
14 +
15 +- Any Carbon Black Cloud product
16 +- Some Alert Types are only generated by specific products
17 +
18 +# Steps
19 +
20 +## Create a custom access level.
21 +
22 +1. Sign in to the VMware Carbon Black Cloud UI console.
23 +2. In the navigation menu, click **Settings > API Access**.
24 +3. On the **API ACCESS** page, in the **Access Levels** tab, click **Add Access Level**.
25 +4. In the dialog, configure these settings:
26 + - **Name** — Enter a memorable name.
27 + - **Description** — Enter a description for the API key.
28 + - **Permissions table** — In the Alerts row, select **READ** for the General information permission.
29 + - Note: This automatically selects **Custom** in the **Copy permissions from** list.
30 +5. Click **Save**.
31 +
32 +## Configure a new API key
33 +
34 +1. In the navigation menu, click **Settings > API Access**.
35 +2. On the **API ACCESS** page, in the **API Keys** tab, click **Add API Key**.
36 +3. In the dialog, configure these settings:
37 + - **Name** — Enter a unique name for the API key. For example, "CoPilot API."
38 + - **Access Level type** — Select **Custom**.
39 + - **Custom Access Level** — Select the access level you created in **Create a custom access level**.
40 +4. Click **Save**.
41 +5. Copy the **API ID** and **API Secret Key** values, and then save them in a safe, encrypted location. You will provide them to CoPilot later.
42 +6. On the **API Keys** tab, copy the **ORG Key** and **ORG ID** values, and then save them in a safe, encrypted location. You will provide them to CoPilot later.
43 +7. In the URL of your VMware Carbon Black Cloud console, copy, and then save the hostname component of the base API URL for your environment. For example, `https://defense.conferdeploy.net`. You will provide this to CoPilot later.
44 + > Tip: See [Constructing your Request](https://developer.carbonblack.com/reference/carbon-black-cloud/authentication/#constructing-your-request) for more information.
backend/app/integrations/mimecast/checkpoint/mimecast_00002.checkpoint deleted
-1
@@ -1 +0,0 @@
1 -eNotjs1ugkAYRd_l25bFID-DJl0MVAy0TVNBwe5w-OTXAYdRi6bvXmy6Puee3DsMyM8SqxwW0O9JewvxLU9K55Sno--PW_MS2U-xW_KG0Znz_d7UL-FuvczTRrabaCzaOugDq1SnbU4FS6-OK17DeL3KvoLVfre0P7yr6SW8-rz1ykUpWBHUfmI-gwbd4TCggoWuwVDhse2Kx4u55RjEMueOBlxipjCujjhJlNgzgxLdNB7of6DGfmJEgwvKoerEXyzjvDuLKQzeJmK6TplF4OcXmjpKpA
backend/app/integrations/modules/docker-compose.yml new
+34
@@ -0,0 +1,34 @@
1 +version: "2"
2 +
3 +services:
4 + copilot-backend:
5 + image: ghcr.io/socfortress/copilot-backend:latest
6 + # Expose the Ports for Graylog Alerting and Docs
7 + ports:
8 + - "5000:5000"
9 + volumes:
10 + - ./data/copilot-backend-data/logs:/opt/logs
11 + # Mount the copilot.db file to persist the database
12 + - ./data/data:/opt/copilot/backend/data
13 + env_file: .env
14 +
15 + copilot-frontend:
16 + image: ghcr.io/socfortress/copilot-frontend:latest
17 + environment:
18 + - SERVER_HOST=${SERVER_HOST:-localhost} # Set the domain name of your server
19 + ports:
20 + - "80:80"
21 + - "443:443"
22 +
23 + copilot-huntress-module:
24 + image: ghcr.io/socfortress/copilot-huntress-module:latest
25 +
26 + copilot-mimecast-module:
27 + image: ghcr.io/socfortress/copilot-mimecast-module:latest
28 +
29 +networks:
30 + default:
31 + driver: bridge
32 + # In case you need to set the MTU
33 + #driver_opts:
34 + # com.docker.network.driver.mtu: "1450"
backend/app/integrations/modules/routes/carbonblack.py new
+101
@@ -0,0 +1,101 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from loguru import logger
4 +from sqlalchemy.ext.asyncio import AsyncSession
5 +
6 +from app.db.db_session import get_db
7 +from app.integrations.modules.schema.carbonblack import CarbonBlackAuthKeys
8 +from app.integrations.modules.schema.carbonblack import CollectCarbonBlack
9 +from app.integrations.modules.schema.carbonblack import InvokeCarbonBlackRequest
10 +from app.integrations.modules.schema.carbonblack import InvokeCarbonBlackResponse
11 +from app.integrations.modules.services.carbonblack import (
12 + post_to_copilot_carbonblack_module,
13 +)
14 +from app.integrations.routes import find_customer_integration
15 +from app.integrations.utils.utils import extract_auth_keys
16 +from app.integrations.utils.utils import get_customer_integration_response
17 +from app.middleware.license import get_license
18 +from app.utils import get_connector_attribute
19 +
20 +module_carbonblack_router = APIRouter()
21 +
22 +
23 +async def get_carbonblack_auth_keys(customer_integration) -> CarbonBlackAuthKeys:
24 + """
25 + Extract the Huntress authentication keys from the CustomerIntegration.
26 +
27 + Args:
28 + customer_integration (CustomerIntegration): The CustomerIntegration containing the
29 + Huntress authentication keys.
30 +
31 + Returns:
32 + CarbonBlackAuthKeys: The extracted Huntress authentication keys.
33 + """
34 + carbonblack_auth_keys = extract_auth_keys(
35 + customer_integration,
36 + service_name="CarbonBlack",
37 + )
38 + logger.info(f"carbonblack_auth_keys: {carbonblack_auth_keys}")
39 +
40 + return CarbonBlackAuthKeys(
41 + carbonblack_api_url=carbonblack_auth_keys["API_URL"],
42 + carbonblack_api_key=carbonblack_auth_keys["API_KEY"],
43 + carbonblack_api_id=carbonblack_auth_keys["API_ID"],
44 + carbonblack_org_key=carbonblack_auth_keys["ORGANIZATION_KEY"],
45 + )
46 +
47 +
48 +async def get_collect_carbonblack_data(carbonblack_request, session, auth_keys):
49 + return CollectCarbonBlack(
50 + integration="carbonblack",
51 + customer_code=carbonblack_request.customer_code,
52 + graylog_host=await get_connector_attribute(
53 + connector_id=14,
54 + column_name="connector_url",
55 + session=session,
56 + ),
57 + graylog_port=await get_connector_attribute(
58 + connector_id=14,
59 + column_name="connector_extra_data",
60 + session=session,
61 + ),
62 + carbonblack_api_url=auth_keys.carbonblack_api_url,
63 + carbonblack_api_key=auth_keys.carbonblack_api_key,
64 + carbonblack_api_id=auth_keys.carbonblack_api_id,
65 + carbonblack_org_key=auth_keys.carbonblack_org_key,
66 + time_range=getattr(auth_keys, "time_range", "-15m"),
67 + )
68 +
69 +
70 +@module_carbonblack_router.post(
71 + "",
72 + response_model=InvokeCarbonBlackResponse,
73 + description="Invoke the Huntress module.",
74 +)
75 +async def collect_carbonblack_route(carbonblack_request: InvokeCarbonBlackRequest, session: AsyncSession = Depends(get_db)):
76 + """Pull down Huntress Events."""
77 + try:
78 + customer_integration_response = await get_customer_integration_response(
79 + carbonblack_request.customer_code,
80 + session,
81 + )
82 +
83 + customer_integration = await find_customer_integration(
84 + carbonblack_request.customer_code,
85 + carbonblack_request.integration_name,
86 + customer_integration_response,
87 + )
88 +
89 + auth_keys = await get_carbonblack_auth_keys(customer_integration)
90 +
91 + collect_carbonblack_data = await get_collect_carbonblack_data(carbonblack_request, session, auth_keys)
92 +
93 + license = await get_license(session)
94 +
95 + await post_to_copilot_carbonblack_module(data=collect_carbonblack_data, license_key=license.license_key)
96 +
97 + except Exception as e:
98 + logger.error(f"Error during DB session: {str(e)}")
99 + return InvokeCarbonBlackResponse(success=False, message=str(e))
100 +
101 + return InvokeCarbonBlackResponse(success=True, message="CarbonBlack Events collected successfully.")
backend/app/integrations/modules/routes/huntress.py new
+105
@@ -0,0 +1,105 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from loguru import logger
4 +from sqlalchemy.ext.asyncio import AsyncSession
5 +
6 +from app.db.db_session import get_db
7 +from app.integrations.modules.schema.huntress import CollectHuntress
8 +from app.integrations.modules.schema.huntress import HuntressAuthKeys
9 +from app.integrations.modules.schema.huntress import InvokeHuntressRequest
10 +from app.integrations.modules.schema.huntress import InvokeHuntressResponse
11 +from app.integrations.modules.services.huntress import post_to_copilot_huntress_module
12 +from app.integrations.routes import find_customer_integration
13 +from app.integrations.utils.utils import extract_auth_keys
14 +from app.integrations.utils.utils import get_customer_integration_response
15 +from app.middleware.license import get_license
16 +from app.utils import get_connector_attribute
17 +
18 +module_huntress_router = APIRouter()
19 +
20 +
21 +async def get_huntress_auth_keys(customer_integration) -> HuntressAuthKeys:
22 + """
23 + Extract the Huntress authentication keys from the CustomerIntegration.
24 +
25 + Args:
26 + customer_integration (CustomerIntegration): The CustomerIntegration containing the
27 + Huntress authentication keys.
28 +
29 + Returns:
30 + HuntressAuthKeys: The extracted Huntress authentication keys.
31 + """
32 + huntress_auth_keys = extract_auth_keys(
33 + customer_integration,
34 + service_name="Huntress",
35 + )
36 +
37 + return HuntressAuthKeys(**huntress_auth_keys)
38 +
39 +
40 +async def get_collect_huntress_data(huntress_request, session, auth_keys):
41 + return CollectHuntress(
42 + integration="huntress",
43 + customer_code=huntress_request.customer_code,
44 + graylog_host=await get_connector_attribute(
45 + connector_id=14,
46 + column_name="connector_url",
47 + session=session,
48 + ),
49 + graylog_port=await get_connector_attribute(
50 + connector_id=14,
51 + column_name="connector_extra_data",
52 + session=session,
53 + ),
54 + wazuh_indexer_host=await get_connector_attribute(
55 + connector_id=1,
56 + column_name="connector_url",
57 + session=session,
58 + ),
59 + wazuh_indexer_username=await get_connector_attribute(
60 + connector_id=1,
61 + column_name="connector_username",
62 + session=session,
63 + ),
64 + wazuh_indexer_password=await get_connector_attribute(
65 + connector_id=1,
66 + column_name="connector_password",
67 + session=session,
68 + ),
69 + api_key=auth_keys.API_KEY,
70 + api_secret=auth_keys.API_SECRET,
71 + )
72 +
73 +
74 +@module_huntress_router.post(
75 + "",
76 + response_model=InvokeHuntressResponse,
77 + description="Invoke the Huntress module.",
78 +)
79 +async def collect_huntress_route(huntress_request: InvokeHuntressRequest, session: AsyncSession = Depends(get_db)):
80 + """Pull down Huntress Events."""
81 + try:
82 + customer_integration_response = await get_customer_integration_response(
83 + huntress_request.customer_code,
84 + session,
85 + )
86 +
87 + customer_integration = await find_customer_integration(
88 + huntress_request.customer_code,
89 + huntress_request.integration_name,
90 + customer_integration_response,
91 + )
92 +
93 + auth_keys = await get_huntress_auth_keys(customer_integration)
94 +
95 + collect_huntress_data = await get_collect_huntress_data(huntress_request, session, auth_keys)
96 +
97 + license = await get_license(session)
98 +
99 + await post_to_copilot_huntress_module(data=collect_huntress_data, license_key=license.license_key)
100 +
101 + except Exception as e:
102 + logger.error(f"Error during DB session: {str(e)}")
103 + return InvokeHuntressResponse(success=False, message=str(e))
104 +
105 + return InvokeHuntressResponse(success=True, message="Huntress Events collected successfully.")
backend/app/integrations/modules/routes/mimecast.py new
+95
@@ -0,0 +1,95 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from loguru import logger
4 +from sqlalchemy.ext.asyncio import AsyncSession
5 +
6 +from app.db.db_session import get_db
7 +from app.integrations.modules.schema.mimecast import CollectMimecast
8 +from app.integrations.modules.schema.mimecast import InvokeMimecastRequest
9 +from app.integrations.modules.schema.mimecast import InvokeMimecastResponse
10 +from app.integrations.modules.schema.mimecast import MimecastAuthKeys
11 +from app.integrations.modules.services.mimecast import post_to_copilot_mimecast_module
12 +from app.integrations.routes import find_customer_integration
13 +from app.integrations.utils.utils import extract_auth_keys
14 +from app.integrations.utils.utils import get_customer_integration_response
15 +from app.middleware.license import get_license
16 +from app.utils import get_connector_attribute
17 +
18 +module_mimecast_router = APIRouter()
19 +
20 +
21 +async def get_mimecast_auth_keys(customer_integration) -> MimecastAuthKeys:
22 + """
23 + Extract the Mimecast authentication keys from the CustomerIntegration.
24 +
25 + Args:
26 + customer_integration (CustomerIntegration): The CustomerIntegration containing the
27 + Mimecast authentication keys.
28 +
29 + Returns:
30 + MimecastAuthKeys: The extracted Huntress authentication keys.
31 + """
32 + mimecast_auth_keys = extract_auth_keys(
33 + customer_integration,
34 + service_name="Mimecast",
35 + )
36 +
37 + return MimecastAuthKeys(**mimecast_auth_keys)
38 +
39 +
40 +async def get_collect_mimecast_data(mimecast_request, session, auth_keys):
41 + return CollectMimecast(
42 + integration="mimecast",
43 + customer_code=mimecast_request.customer_code,
44 + graylog_host=await get_connector_attribute(
45 + connector_id=14,
46 + column_name="connector_url",
47 + session=session,
48 + ),
49 + graylog_port=await get_connector_attribute(
50 + connector_id=14,
51 + column_name="connector_extra_data",
52 + session=session,
53 + ),
54 + app_id=auth_keys.APP_ID,
55 + app_key=auth_keys.APP_KEY,
56 + email_address=auth_keys.EMAIL_ADDRESS,
57 + access_key=auth_keys.ACCESS_KEY,
58 + secret_key=auth_keys.SECRET_KEY,
59 + uri=auth_keys.URI,
60 + time_range="15m",
61 + )
62 +
63 +
64 +@module_mimecast_router.post(
65 + "",
66 + response_model=InvokeMimecastResponse,
67 + description="Invoke the Huntress module.",
68 +)
69 +async def collect_huntress_route(mimecast_request: InvokeMimecastRequest, session: AsyncSession = Depends(get_db)):
70 + """Pull down Huntress Events."""
71 + try:
72 + customer_integration_response = await get_customer_integration_response(
73 + mimecast_request.customer_code,
74 + session,
75 + )
76 +
77 + customer_integration = await find_customer_integration(
78 + mimecast_request.customer_code,
79 + mimecast_request.integration_name,
80 + customer_integration_response,
81 + )
82 +
83 + auth_keys = await get_mimecast_auth_keys(customer_integration)
84 +
85 + collect_huntress_data = await get_collect_mimecast_data(mimecast_request, session, auth_keys)
86 +
87 + license = await get_license(session)
88 +
89 + await post_to_copilot_mimecast_module(data=collect_huntress_data, license_key=license.license_key)
90 +
91 + except Exception as e:
92 + logger.error(f"Error during DB session: {str(e)}")
93 + return InvokeMimecastResponse(success=False, message=str(e))
94 +
95 + return InvokeMimecastResponse(success=True, message="Mimecast Events collected successfully.")
backend/app/integrations/modules/schema/carbonblack.py new
+68
@@ -0,0 +1,68 @@
1 +from typing import Optional
2 +
3 +from fastapi import HTTPException
4 +from pydantic import BaseModel
5 +from pydantic import Field
6 +from pydantic import validator
7 +
8 +
9 +class InvokeCarbonBlackRequest(BaseModel):
10 + customer_code: str = Field(
11 + ...,
12 + description="The customer code.",
13 + examples=["00002"],
14 + )
15 + integration_name: str = Field(
16 + "CarbonBlack",
17 + description="The integration name.",
18 + examples=["CarbonBlack"],
19 + )
20 +
21 +
22 +class CarbonBlackAuthKeys(BaseModel):
23 + carbonblack_api_url: str = Field(..., example="https://127.0.0.1")
24 + carbonblack_api_key: str = Field(..., example="1234567890")
25 + carbonblack_api_id: str = Field(..., example="1234567890")
26 + carbonblack_org_key: str = Field(..., example="1234567890")
27 + time_range: Optional[str] = Field(
28 + "-15m",
29 + example="-15m",
30 + description="The time range to collect events.",
31 + )
32 +
33 +
34 +class InvokeCarbonBlackResponse(BaseModel):
35 + success: bool = Field(
36 + ...,
37 + description="The success status.",
38 + examples=[True],
39 + )
40 + message: str = Field(
41 + ...,
42 + description="The message.",
43 + examples=["CarbonBlack Events collected successfully."],
44 + )
45 +
46 +
47 +class CollectCarbonBlack(BaseModel):
48 + integration: str = Field(..., example="carbonblack")
49 + customer_code: str = Field(..., example="socfortress")
50 + graylog_host: str = Field(..., example="127.0.0.1")
51 + graylog_port: str = Field(..., example=12201)
52 + carbonblack_api_url: str = Field(..., example="https://127.0.0.1")
53 + carbonblack_api_key: str = Field(..., example="1234567890")
54 + carbonblack_api_id: str = Field(..., example="1234567890")
55 + carbonblack_org_key: str = Field(..., example="1234567890")
56 + time_range: Optional[str] = Field(
57 + "-15m",
58 + example="-15m",
59 + )
60 +
61 + @validator("integration")
62 + def check_integration(cls, v):
63 + if v != "carbonblack":
64 + raise HTTPException(
65 + status_code=400,
66 + detail="Invalid integration. Only 'carbonblack' is supported.",
67 + )
68 + return v
backend/app/integrations/modules/schema/huntress.py new
+64
@@ -0,0 +1,64 @@
1 +from fastapi import HTTPException
2 +from pydantic import BaseModel
3 +from pydantic import Field
4 +from pydantic import validator
5 +
6 +
7 +class InvokeHuntressRequest(BaseModel):
8 + customer_code: str = Field(
9 + ...,
10 + description="The customer code.",
11 + examples=["00002"],
12 + )
13 + integration_name: str = Field(
14 + "Huntress",
15 + description="The integration name.",
16 + examples=["Huntress"],
17 + )
18 +
19 +
20 +class HuntressAuthKeys(BaseModel):
21 + API_KEY: str = Field(
22 + ...,
23 + description="The API key.",
24 + examples=["123456"],
25 + )
26 + API_SECRET: str = Field(
27 + ...,
28 + description="The secret key.",
29 + examples=["123456"],
30 + )
31 +
32 +
33 +class InvokeHuntressResponse(BaseModel):
34 + success: bool = Field(
35 + ...,
36 + description="The success status.",
37 + examples=[True],
38 + )
39 + message: str = Field(
40 + ...,
41 + description="The message.",
42 + examples=["Huntress Events collected successfully."],
43 + )
44 +
45 +
46 +class CollectHuntress(BaseModel):
47 + integration: str = Field(..., example="huntress")
48 + customer_code: str = Field(..., example="socfortress")
49 + graylog_host: str = Field(..., example="127.0.0.1")
50 + graylog_port: str = Field(..., example=12201)
51 + wazuh_indexer_host: str = Field(..., example="127.0.0.1")
52 + wazuh_indexer_username: str = Field(..., example="admin")
53 + wazuh_indexer_password: str = Field(..., example="admin")
54 + api_key: str = Field(..., example="1234567890")
55 + api_secret: str = Field(..., example="1234567890")
56 +
57 + @validator("integration")
58 + def check_integration(cls, v):
59 + if v != "huntress":
60 + raise HTTPException(
61 + status_code=400,
62 + detail="Invalid integration. Only 'huntress' is supported.",
63 + )
64 + return v
backend/app/integrations/modules/schema/mimecast.py new
+116
@@ -0,0 +1,116 @@
1 +from typing import Optional
2 +
3 +from fastapi import HTTPException
4 +from pydantic import BaseModel
5 +from pydantic import Field
6 +from pydantic import validator
7 +
8 +
9 +class InvokeMimecastRequest(BaseModel):
10 + customer_code: str = Field(
11 + ...,
12 + description="The customer code.",
13 + examples=["00002"],
14 + )
15 + integration_name: str = Field(
16 + "Mimecast",
17 + description="The integration name.",
18 + examples=["Mimecast"],
19 + )
20 +
21 +
22 +class MimecastAuthKeys(BaseModel):
23 + APP_ID: str = Field(
24 + ...,
25 + description="YOUR DEVELOPER APPLICATION ID",
26 + examples=["00002"],
27 + )
28 + APP_KEY: str = Field(
29 + ...,
30 + description="YOUR DEVELOPER APPLICATION KEY",
31 + examples=["00002"],
32 + )
33 + EMAIL_ADDRESS: Optional[str] = Field(
34 + None,
35 + description="EMAIL ADDRESS OF YOUR ADMINISTRATOR",
36 + examples=["00002"],
37 + )
38 + ACCESS_KEY: str = Field(
39 + ...,
40 + description="ACCESS KEY FOR YOUR ADMINISTRATOR",
41 + examples=["00002"],
42 + )
43 + SECRET_KEY: str = Field(
44 + ...,
45 + description="SECRET KEY FOR YOUR ADMINISTRATOR",
46 + examples=["00002"],
47 + )
48 + URI = str = Field(
49 + "/api/audit/get-siem-logs",
50 + description="URI FOR YOUR API Endpoint",
51 + examples=["/api/audit/get-siem-logs"],
52 + )
53 +
54 +
55 +class InvokeMimecastResponse(BaseModel):
56 + success: bool = Field(
57 + ...,
58 + description="The success status.",
59 + examples=[True],
60 + )
61 + message: str = Field(
62 + ...,
63 + description="The message.",
64 + examples=["Mimecast Events collected successfully."],
65 + )
66 +
67 +
68 +class CollectMimecast(BaseModel):
69 + integration: str = Field(..., example="mimecast")
70 + customer_code: str = Field(..., example="socfortress")
71 + graylog_host: str = Field(..., example="127.0.0.1")
72 + graylog_port: str = Field(..., example=12201)
73 + app_id: str = Field(
74 + ...,
75 + description="YOUR DEVELOPER APPLICATION ID",
76 + examples=["00002"],
77 + )
78 + app_key: str = Field(
79 + ...,
80 + description="YOUR DEVELOPER APPLICATION KEY",
81 + examples=["00002"],
82 + )
83 + email_address: Optional[str] = Field(
84 + None,
85 + description="EMAIL ADDRESS OF YOUR ADMINISTRATOR",
86 + examples=["00002"],
87 + )
88 + access_key: str = Field(
89 + ...,
90 + description="ACCESS KEY FOR YOUR ADMINISTRATOR",
91 + examples=["00002"],
92 + )
93 + secret_key: str = Field(
94 + ...,
95 + description="SECRET KEY FOR YOUR ADMINISTRATOR",
96 + examples=["00002"],
97 + )
98 + uri: str = Field(
99 + "/api/audit/get-siem-logs",
100 + description="URI FOR YOUR API Endpoint",
101 + examples=["/api/audit/get-siem-logs"],
102 + )
103 + time_range: Optional[str] = Field(
104 + "15m",
105 + pattern="^[1-9][0-9]*[mhdw]$",
106 + description="Time range for the query (1m, 1h, 1d, 1w)",
107 + )
108 +
109 + @validator("integration")
110 + def check_integration(cls, v):
111 + if v != "mimecast":
112 + raise HTTPException(
113 + status_code=400,
114 + detail="Invalid integration. Only 'mimecast' is supported.",
115 + )
116 + return v
backend/app/integrations/modules/services/carbonblack.py new
+22
@@ -0,0 +1,22 @@
1 +import httpx
2 +from loguru import logger
3 +
4 +from app.integrations.modules.schema.carbonblack import CollectCarbonBlack
5 +
6 +
7 +async def post_to_copilot_carbonblack_module(data: CollectCarbonBlack, license_key: str):
8 + """
9 + Send a POST request to the copilot-huntress-module Docker container.
10 +
11 + Args:
12 + data (CollectHuntress): The data to send to the copilot-huntress-module Docker container.
13 + """
14 + logger.info(f"Sending POST request to http://copilot-carbonblack-module/collect with data: {data.dict()}")
15 + async with httpx.AsyncClient() as client:
16 + await client.post(
17 + "http://copilot-carbonblack-module/collect",
18 + json=data.dict(),
19 + params={"license_key": license_key, "feature_name": "CARBONBLACK"},
20 + timeout=120,
21 + )
22 + return None
backend/app/integrations/modules/services/huntress.py new
+22
@@ -0,0 +1,22 @@
1 +import httpx
2 +from loguru import logger
3 +
4 +from app.integrations.modules.schema.huntress import CollectHuntress
5 +
6 +
7 +async def post_to_copilot_huntress_module(data: CollectHuntress, license_key: str):
8 + """
9 + Send a POST request to the copilot-huntress-module Docker container.
10 +
11 + Args:
12 + data (CollectHuntress): The data to send to the copilot-huntress-module Docker container.
13 + """
14 + logger.info(f"Sending POST request to http://copilot-huntress-module/collect with data: {data.dict()}")
15 + async with httpx.AsyncClient() as client:
16 + await client.post(
17 + "http://copilot-huntress-module/collect",
18 + json=data.dict(),
19 + params={"license_key": license_key, "feature_name": "HUNTRESS"},
20 + timeout=120,
21 + )
22 + return None
backend/app/integrations/modules/services/mimecast.py new
+22
@@ -0,0 +1,22 @@
1 +import httpx
2 +from loguru import logger
3 +
4 +from app.integrations.modules.schema.mimecast import CollectMimecast
5 +
6 +
7 +async def post_to_copilot_mimecast_module(data: CollectMimecast, license_key: str):
8 + """
9 + Send a POST request to the copilot-mimecast-module Docker container.
10 +
11 + Args:
12 + data (CollectMimecast): The data to send to the copilot-mimecast-module Docker container.
13 + """
14 + logger.info(f"Sending POST request to http://copilot-huntress-module/collect with data: {data.dict()}")
15 + async with httpx.AsyncClient() as client:
16 + await client.post(
17 + "http://copilot-mimecast-module/collect",
18 + json=data.dict(),
19 + params={"license_key": license_key, "feature_name": "MIMECAST"},
20 + timeout=120,
21 + )
22 + return None
backend/app/middleware/license.py
+504 -272
@@ -1,6 +1,5 @@
1 import os
2 from datetime import datetime as dt
3 -from enum import Enum
3 from typing import Any
4 from typing import Dict
5 from typing import List
@@ -10,11 +9,6 @@ import requests
9 from fastapi import APIRouter
10 from fastapi import Depends
11 from fastapi import HTTPException
13 -from licensing.methods import Data
14 -from licensing.methods import Helpers
15 -from licensing.methods import Key
16 -
17 -# from licensing.models import *
12 from loguru import logger
13 from pydantic import BaseModel
14 from pydantic import Field
@@ -60,25 +54,18 @@ class ReplaceLicenseRequest(BaseModel):
54 license_key: str = Field(..., title="The license key to replace")
55
56
63 -class CreateLicenseRequest(BaseModel):
64 - """
65 - A Pydantic model for creating a license.
57 +class TrialLicenseRequest(BaseModel):
58 + period: Optional[int] = Field(7, title="The period of the trial license")
59 + email: str = Field(..., title="The email of the user")
60 + feature_name: str = Field(..., title="The feature name")
61 + customer_name: str = Field(..., title="The customer name")
62 + company_name: str = Field(..., title="The company name")
63
67 - Attributes:
68 - product_id (int): The product id.
69 - notes (str): The notes.
70 - new_customer (bool): Whether the customer is new.
71 - name (str): The customer name.
72 - email (str): The customer email.
73 - company_name (str): The customer company name.
74 - """
64
76 - product_id: int = Field(24355, title="The product id")
77 - notes: str = Field("Test Key", title="The notes")
78 - new_customer: bool = Field(True, title="Whether the customer is new")
79 - name: str = Field("Test Customer", title="The customer name")
80 - email: str = Field(..., title="The customer email")
81 - company_name: str = Field("Test Company", title="The customer company name")
65 +class TrialLicenseResponse(BaseModel):
66 + license_key: str
67 + success: bool
68 + message: str
69
70
71 class CreateCustomerKeyResult(BaseModel):
@@ -99,11 +86,11 @@ class CreateCustomerKeyRouteResponse(BaseModel):
86
87
88 class Customer(BaseModel):
102 - Id: int
103 - Name: str
104 - Email: str
105 - CompanyName: str
106 - Created: int
89 + id: int
90 + name: str
91 + email: str
92 + companyName: str
93 + created: dt
94
95
96 class RawResponse(BaseModel):
@@ -115,7 +102,7 @@ class RawResponse(BaseModel):
102
103
104 class LicenseResponse(BaseModel):
118 - product_id: int
105 + productId: int
106 id: int
107 key: str
108 created: dt
@@ -131,15 +118,15 @@ class LicenseResponse(BaseModel):
118 f8: bool
119 notes: str
120 block: bool
134 - global_id: int
121 + globalId: int
122 customer: Customer
136 - activated_machines: List
137 - trial_activation: bool
138 - max_no_of_machines: int
139 - allowed_machines: Optional[Any]
140 - data_objects: List
141 - sign_date: dt
142 - reseller: Optional[Any]
123 + activatedMachines: List
124 + trialActivation: bool
125 + maxNoOfMachines: int
126 + allowedMachines: Optional[Any]
127 + dataObjects: List
128 + signDate: dt
129 + reseller: Optional[Any] = None
130
131
132 class VerifyLicenseResponse(BaseModel):
@@ -160,36 +147,171 @@ class GetLicenseFeaturesResponse(BaseModel):
147 message: str
148
149
163 -class Feature(Enum):
164 - MIMECAST = "MIMECAST"
165 - SAP_SIEM = "SAP SIEM"
166 - HUNTRESS = "HUNTRESS"
167 - REPORTING = "REPORTING"
168 - # Add more features as needed
169 -
170 - @classmethod
171 - def get_feature_name(cls, feature_name):
172 - feature_map = {
173 - cls.MIMECAST.value: "MIMECAST",
174 - cls.SAP_SIEM.value: "SAP SIEM",
175 - cls.HUNTRESS.value: "HUNTRESS",
176 - cls.REPORTING.value: "REPORTING",
177 - # Add more mappings as needed
178 - }
179 - return feature_map.get(feature_name)
150 +class IsFeatureEnabledResponse(BaseModel):
151 + enabled: bool
152 + success: bool
153 + message: str
154
155
182 -class SubscriptionCatalog(str, Enum):
183 - """
184 - The subscription catalog.
185 - """
156 +class Feature(BaseModel):
157 + id: int
158 + subscription_price_id: str
159 + name: str
160 + price: int
161 + currency: str
162 + info: str
163 + short_description: str
164 + full_description: str
165
187 - MIMECAST = (
188 - "Integrate your SIEM stack with Mimecast to detect and respond to advanced threats."
189 - "This integration includes ingesting of Mimecast logs into your SIEM stack, Grafana dashboards,"
190 - "and alerts for advanced threat detection.",
191 - )
192 - HUNTRESS = "Integrate your SIEM stack with Huntress to detect and respond to advanced threats."
166 +
167 +class GetSubscriptionCatalogFeaturesResponse(BaseModel):
168 + features: List[Feature]
169 + success: bool
170 + message: str
171 +
172 +
173 +class FeatureSubscriptionRequest(BaseModel):
174 + feature_id: int = Field(..., example=1)
175 + cancel_url: str = Field(..., example="https://example.com/cancel")
176 + success_url: str = Field(..., example="https://example.com/success")
177 + customer_email: str = Field(..., example="info@socfortress.co")
178 + company_name: str = Field(..., example="SOCFORTRESS")
179 +
180 +
181 +class GetLicenseByEmailRequest(BaseModel):
182 + email: str = Field(..., example="info@socfortress.co")
183 +
184 +
185 +class AddLicenseToDB(BaseModel):
186 + customer_name: str
187 + customer_email: str
188 + company_name: str
189 +
190 +
191 +###### ! CREATE SESSION CHECKOUT ! ######
192 +class AutomaticTax(BaseModel):
193 + enabled: bool
194 + liability: Optional[str] = None
195 + status: Optional[str] = None
196 +
197 +
198 +class CustomText(BaseModel):
199 + after_submit: Optional[str] = None
200 + shipping_address: Optional[str] = None
201 + submit: Optional[str] = None
202 + terms_of_service_acceptance: Optional[str] = None
203 +
204 +
205 +class InvoiceData(BaseModel):
206 + account_tax_ids: Optional[str] = None
207 + custom_fields: Optional[str] = None
208 + description: Optional[str] = None
209 + footer: Optional[str] = None
210 + issuer: Optional[str] = None
211 + metadata: Dict = {}
212 + rendering_options: Optional[str] = None
213 +
214 +
215 +class InvoiceCreation(BaseModel):
216 + enabled: bool
217 + invoice_data: InvoiceData
218 +
219 +
220 +class PaymentMethodOptionsCard(BaseModel):
221 + request_three_d_secure: str
222 +
223 +
224 +class PaymentMethodOptions(BaseModel):
225 + card: PaymentMethodOptionsCard
226 +
227 +
228 +class PhoneNumberCollection(BaseModel):
229 + enabled: bool
230 +
231 +
232 +class TotalDetails(BaseModel):
233 + amount_discount: int
234 + amount_shipping: int
235 + amount_tax: int
236 +
237 +
238 +class CustomerDetails(BaseModel):
239 + address: Optional[str] = None
240 + email: Optional[str] = None
241 + name: Optional[str] = None
242 + phone: Optional[str] = None
243 + tax_exempt: Optional[str] = None
244 + tax_ids: Optional[str] = None
245 +
246 +
247 +class CheckoutSession(BaseModel):
248 + after_expiration: Optional[str] = None
249 + allow_promotion_codes: Optional[str] = None
250 + amount_subtotal: int
251 + amount_total: int
252 + automatic_tax: AutomaticTax
253 + billing_address_collection: Optional[str] = None
254 + cancel_url: str
255 + client_reference_id: Optional[str] = None
256 + client_secret: Optional[str] = None
257 + consent: Optional[str] = None
258 + consent_collection: Optional[str] = None
259 + created: int
260 + currency: str
261 + currency_conversion: Optional[str] = None
262 + custom_fields: List = []
263 + custom_text: CustomText
264 + customer: Optional[str] = None
265 + customer_creation: Optional[str] = None
266 + customer_details: Optional[CustomerDetails] = None
267 + customer_email: Optional[str] = None
268 + expires_at: int
269 + id: str
270 + invoice: Optional[str] = None
271 + invoice_creation: Optional[InvoiceCreation] = None
272 + livemode: bool
273 + locale: Optional[str] = None
274 + metadata: Dict
275 + mode: str
276 + object: str
277 + payment_intent: Optional[str] = None
278 + payment_link: Optional[str] = None
279 + payment_method_collection: str
280 + payment_method_configuration_details: Optional[str] = None
281 + payment_method_options: PaymentMethodOptions
282 + payment_method_types: List[str]
283 + payment_status: str
284 + phone_number_collection: PhoneNumberCollection
285 + recovered_from: Optional[str] = None
286 + setup_intent: Optional[str] = None
287 + shipping_address_collection: Optional[str] = None
288 + shipping_cost: Optional[str] = None
289 + shipping_details: Optional[str] = None
290 + shipping_options: List = []
291 + status: str
292 + submit_type: Optional[str] = None
293 + subscription: Optional[str] = None
294 + success_url: str
295 + total_details: TotalDetails
296 + ui_mode: str
297 + url: str
298 +
299 +
300 +class CheckoutSessionResponse(BaseModel):
301 + success: bool = True
302 + message: str = "Checkout session created successfully"
303 + session: CheckoutSession
304 +
305 +
306 +class CancelSubscriptionRequest(BaseModel):
307 + customer_email: str
308 + subscription_price_id: str
309 + feature_name: str
310 +
311 +
312 +class CancelSubscriptionResponse(BaseModel):
313 + success: bool
314 + message: str
315
316
317 license_router = APIRouter()
@@ -211,58 +333,23 @@ def get_auth_token():
333 return auth
334
335
214 -def get_rsa_pub_key():
215 - rsa_public_key = os.getenv("RSA_PUBLIC_KEY")
216 - if not rsa_public_key:
217 - raise HTTPException(status_code=500, detail="RSA public key not found")
218 - return rsa_public_key
219 -
220 -
221 -def get_product_id():
222 - product_id = os.getenv("PRODUCT_ID")
223 - if not product_id:
224 - raise HTTPException(status_code=500, detail="Product id not found")
225 - return product_id
226 -
227 -
228 -def create_trial_key(auth, request):
229 - result, _ = Key.create_key(
230 - token=auth,
231 - product_id=request.product_id,
232 - period=7,
233 - notes=request.notes,
234 - new_customer=request.new_customer,
235 - name=request.name,
236 - email=request.email,
237 - company_name=request.company_name,
238 - )
239 - logger.info(result)
240 - result = CreateCustomerKeyResponseModel(response=[result])
241 - return result
242 -
243 -
244 -def create_key(auth, request):
245 - result, _ = Key.create_key(
246 - token=auth,
247 - product_id=request.product_id,
248 - period=365,
249 - notes=request.notes,
250 - new_customer=request.new_customer,
251 - name=request.name,
252 - email=request.email,
253 - company_name=request.company_name,
254 - )
255 - result = CreateCustomerKeyResponseModel(response=[result])
256 - return result
336 +async def add_license_to_db(session: AsyncSession, result, request: AddLicenseToDB):
337 + """
338 + Add a new license to the database.
339
340 + :param session: AsyncSession object for the database session
341 + :param result: The license key to be added
342 + :param request: The request object containing customer details
343 + :return: The newly added License object
344 + """
345
259 -async def add_license_to_db(session: AsyncSession, result, request):
346 new_license = License(
261 - license_key=result.response[0].key,
262 - customer_name=request.name,
263 - customer_email=request.email,
347 + license_key=result,
348 + customer_name=request.customer_name,
349 + customer_email=request.customer_email,
350 company_name=request.company_name,
351 )
352 +
353 logger.info(f"Adding new license: {new_license} to the database")
354 session.add(new_license)
355 await session.commit()
@@ -270,38 +357,18 @@ async def add_license_to_db(session: AsyncSession, result, request):
357
358
359 async def get_license(session: AsyncSession) -> License:
273 - try:
274 - result = await session.execute(select(License))
275 - license = result.scalars().first()
276 - if not license:
277 - raise HTTPException(status_code=404, detail="No license found")
278 - return license
279 - except Exception as e:
280 - logger.error(e)
281 - raise HTTPException(status_code=404, detail="No license found")
282 -
283 -
284 -def check_license(license: License):
285 - logger.info(f"Checking license: {license}")
286 - result, _ = Key.activate(
287 - token=get_auth_token(),
288 - rsa_pub_key=get_rsa_pub_key(),
289 - product_id=get_product_id(),
290 - key=license.license_key,
291 - machine_code=Helpers.GetMachineCode(v=2),
292 - )
293 - return result
294 -
360 + """
361 + Get the license from the database
362
296 -def extend_license(license: License, period: int):
297 - result, _ = Key.extend_license(
298 - token=get_auth_token(),
299 - product_id=get_product_id(),
300 - key=license.license_key,
301 - no_of_days=period,
302 - )
303 - logger.info(result)
304 - return result
363 + :param session: The AsyncSession object for the database
364 + :return: The License object
365 + """
366 + result = await session.execute(select(License))
367 + license = result.scalars().first()
368 + if license is None:
369 + raise HTTPException(status_code=404, detail="No license found")
370 + else:
371 + return license
372
373
374 def is_license_expired(license: dict) -> bool:
@@ -314,7 +381,9 @@ def is_license_expired(license: dict) -> bool:
381 Returns:
382 bool: True if the license is expired, False otherwise.
383 """
317 - return dt.now() > license["expires"]
384 + logger.info(f"License: {license}")
385 + expires = dt.strptime(license["data"]["license"]["expires"], "%Y-%m-%dT%H:%M:%S.%f")
386 + return dt.now() > expires
387
388
389 async def is_feature_enabled(feature_name: str, session: AsyncSession) -> bool:
@@ -330,83 +399,241 @@ async def is_feature_enabled(feature_name: str, session: AsyncSession) -> bool:
399 bool: True if the feature is enabled, False otherwise.
400 """
401 license = await get_license(session)
333 - license_details = LicenseResponse(**check_license(license).__dict__)
334 - for data_object in license_details.data_objects:
335 - if data_object["Name"] == feature_name and data_object["IntValue"] == 1:
402 + result = await send_post_request("verify-license", data={"license_key": license.license_key})
403 + for data_object in result["data"]["license"]["dataObjects"]:
404 + if data_object["name"] == feature_name and data_object["intValue"] == 1:
405 return True
406
407 raise HTTPException(status_code=400, detail="Feature not enabled. You must purchase a license to use this feature.")
408
409
410 +async def send_get_request(endpoint: str) -> Dict[str, Any]:
411 + """
412 + Sends a GET request to the Shuffle service.
413 +
414 + Args:
415 + endpoint (str): The endpoint to send the GET request to.
416 +
417 + Returns:
418 + Dict[str, Any]: The response from the GET request.
419 + """
420 + logger.info(f"Sending GET request to {endpoint}")
421 +
422 + try:
423 + HEADERS = {
424 + "x-api-key": f"{os.getenv('COPILOT_API_KEY')}",
425 + "Content-Type": "application/json",
426 + "module-version": "1.0",
427 + }
428 + response = requests.get(
429 + f"https://license.socfortress.co/{endpoint}",
430 + headers=HEADERS,
431 + verify=False,
432 + )
433 +
434 + if response.status_code == 204:
435 + return {
436 + "data": None,
437 + "success": True,
438 + "message": "Successfully completed request with no content",
439 + }
440 + else:
441 + return {
442 + "data": response.json(),
443 + "success": False if response.status_code >= 400 else True,
444 + "message": "Successfully retrieved data",
445 + }
446 + except Exception as e:
447 + logger.error(f"Failed to send GET request to {endpoint} with error: {e}")
448 + raise HTTPException(
449 + status_code=500,
450 + detail=f"Failed to send GET request to {endpoint} with error: {e}",
451 + )
452 +
453 +
454 +@license_router.get(
455 + "/subscription_features",
456 + description="Get the subscription features available",
457 + response_model=GetSubscriptionCatalogFeaturesResponse,
458 +)
459 +async def get_subscription_catalog():
460 + """
461 + Get the subscription catalog. This is handled by the Middleware running in SOCFortress Infra
462 +
463 + Returns:
464 + dict: A dictionary containing the subscription catalog.
465 + """
466 + try:
467 + results = await send_get_request("features")
468 + return GetSubscriptionCatalogFeaturesResponse(
469 + features=results["data"]["features"],
470 + success=results["success"],
471 + message=results["message"],
472 + )
473 + except Exception as e:
474 + logger.error(e)
475 + raise HTTPException(status_code=400, detail="Failed to get subscription features")
476 +
477 +
478 @license_router.post(
342 - "/create_trial_key",
343 - description="Create a trial license key",
479 + "/retrieve_license_by_email",
480 + description="Retrieve a license by email",
481 + response_model=GetLicenseResponse,
482 )
345 -async def create_trial_license_key(request: CreateLicenseRequest, session: AsyncSession = Depends(get_db)):
483 +async def retrieve_license_by_email(request: GetLicenseByEmailRequest, session: AsyncSession = Depends(get_db)) -> GetLicenseResponse:
484 """
347 - Create a trial license key.
485 + Retrieve a license by email.
486
487 Args:
350 - request (CreateLicenseRequest): The request containing the license key to create.
488 + request (GetLicenseRequest): The request containing the email to retrieve the license by.
489 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
490
491 Returns:
354 - LicenseVerificationResponse: A Pydantic model containing the verification status and message.
492 + GetLicenseResponse: A Pydantic model containing the license key, success status, and message.
493 """
356 - await check_if_license_exists(session)
357 - auth = get_auth_token()
358 - result = create_trial_key(auth, request)
359 - await add_license_to_db(session, result, request)
360 - return result
494 + # Check if a license with the given email already exists in the database
495 + result = await session.execute(select(License).where(License.customer_email == request.email))
496 + existing_license = result.scalars().first()
497 + if existing_license:
498 + return GetLicenseResponse(
499 + license_key=existing_license.license_key,
500 + success=True,
501 + message="License retrieved successfully",
502 + )
503 +
504 + results = await send_post_request("retrieve-license-by-email", data={"email": request.email})
505 + logger.info(f"Results: {results}")
506 + if results["data"]["success"] is False:
507 + raise HTTPException(status_code=400, detail=f"Failed to retrieve license by email: {results['data']['message']}")
508 +
509 + # Add the license to the database
510 + await add_license_to_db(
511 + session,
512 + results["data"]["license"]["key"],
513 + AddLicenseToDB(
514 + customer_email=results["data"]["license"]["customer"]["email"],
515 + customer_name=results["data"]["license"]["customer"]["name"],
516 + company_name=results["data"]["license"]["customer"]["companyName"],
517 + ),
518 + )
519 + return GetLicenseResponse(
520 + license_key=results["data"]["license"]["key"],
521 + success=results["data"]["success"],
522 + message=results["data"]["message"],
523 + )
524
525
526 @license_router.post(
364 - "/create_new_key",
365 - response_model=CreateCustomerKeyRouteResponse,
366 - description="Create a new license key",
527 + "/create_checkout_session",
528 + description="Create a checkout session",
529 + response_model=CheckoutSessionResponse,
530 )
368 -async def create_new_license_key(request: CreateLicenseRequest, session: AsyncSession = Depends(get_db)) -> CreateCustomerKeyRouteResponse:
531 +async def create_checkout_session(request: FeatureSubscriptionRequest):
532 """
370 - Create a new license key.
533 + Create a checkout session.
534
535 Args:
373 - license_key (str): The license key to verify.
536 + request (FeatureSubscriptionRequest): The request containing the feature id and user id.
537 +
538 + Returns:
539 + dict: A dictionary containing the checkout session.
540 + """
541 + results = await send_post_request(
542 + "create-checkout-session",
543 + data={
544 + "feature_id": request.feature_id,
545 + "cancel_url": request.cancel_url,
546 + "success_url": request.success_url,
547 + "customer_email": request.customer_email,
548 + "company_name": request.company_name,
549 + },
550 + )
551 + logger.info(f"Results: {results}")
552 + if results["data"]["success"] is False:
553 + raise HTTPException(status_code=400, detail=f"Failed to create checkout session: {results['data']['message']}")
554 + return CheckoutSessionResponse(
555 + session=results["data"]["session"],
556 + success=results["data"]["success"],
557 + message=results["data"]["message"],
558 + )
559 +
560 +
561 +@license_router.post(
562 + "/trial_license",
563 + description="Create a trial license",
564 + response_model=TrialLicenseResponse,
565 +)
566 +async def create_trial_license_key(request: TrialLicenseRequest, session: AsyncSession = Depends(get_db)) -> TrialLicenseResponse:
567 + """
568 + Create a trial license key.
569 +
570 + Args:
571 + request (CreateLicenseRequest): The request containing the license key to create.
572 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
573
574 Returns:
575 LicenseVerificationResponse: A Pydantic model containing the verification status and message.
576 """
577 await check_if_license_exists(session)
380 - auth = get_auth_token()
381 - result = create_key(auth, request)
382 - logger.info(f"Result: {result}")
383 - await add_license_to_db(session, result, request)
384 - return CreateCustomerKeyRouteResponse(response=result.response, success=True, message="License created successfully")
578 + results = await send_post_request(
579 + "trial-license",
580 + data={
581 + "email": request.email,
582 + "feature_name": request.feature_name,
583 + "customer_name": request.customer_name,
584 + "period": request.period,
585 + "company_name": request.company_name,
586 + },
587 + )
588 + logger.info(f"Results: {results}")
589 + if results["data"]["success"] is False:
590 + raise HTTPException(status_code=400, detail=f"Failed to create trial license: {results['data']['message']}")
591 + await add_license_to_db(
592 + session,
593 + results["data"]["license_key"],
594 + AddLicenseToDB(
595 + customer_email=request.email,
596 + customer_name=request.customer_name,
597 + company_name=request.company_name,
598 + ),
599 + )
600 + return TrialLicenseResponse(
601 + license_key=results["data"]["license_key"],
602 + success=results["data"]["success"],
603 + message=results["data"]["message"],
604 + )
605
606
607 @license_router.post(
388 - "/extend_license",
389 - description="Extend a license",
608 + "/cancel_subscription",
609 + description="Cancel a subscription",
610 + response_model=CancelSubscriptionResponse,
611 )
391 -async def extend_license_key(period: int, session: AsyncSession = Depends(get_db)):
612 +async def cancel_subscription(request: CancelSubscriptionRequest) -> CancelSubscriptionResponse:
613 """
393 - Extend a license key.
614 + Cancel a subscription.
615
616 Args:
396 - period (int): The period to extend the license by.
397 - session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
617 + request (CancelSubscriptionRequest): The request containing the customer email, subscription price id, and feature name.
618
619 Returns:
400 - LicenseVerificationResponse: A Pydantic model containing the verification status and message.
620 + dict: A dictionary containing the cancellation status.
621 """
402 - try:
403 - license = await get_license(session)
404 - logger.info(f"License: {license}")
405 - extend_license(license, period)
406 - return {"message": "License extended successfully", "success": True}
407 - except Exception as e:
408 - logger.error(e)
409 - raise HTTPException(status_code=400, detail="License extension failed")
622 + results = await send_post_request(
623 + "cancel-subscription",
624 + data={
625 + "customer_email": request.customer_email,
626 + "subscription_price_id": request.subscription_price_id,
627 + "feature_name": request.feature_name,
628 + },
629 + )
630 + logger.info(f"Results: {results}")
631 + if results["data"]["success"] is False:
632 + raise HTTPException(status_code=400, detail=f"Failed to cancel subscription: {results['data']['message']}")
633 + return CancelSubscriptionResponse(
634 + success=results["data"]["success"],
635 + message=results["data"]["message"],
636 + )
637
638
639 @license_router.get(
@@ -426,13 +653,10 @@ async def verify_license_key(session: AsyncSession = Depends(get_db)) -> VerifyL
653 """
654 license = await get_license(session)
655 try:
429 - logger.info(f"License: {license}")
430 - result = check_license(license)
431 - result = result.__dict__
432 - logger.info(result)
656 + result = await send_post_request("verify-license", data={"license_key": license.license_key})
657 if is_license_expired(result):
658 raise HTTPException(status_code=400, detail="License is expired")
435 - return VerifyLicenseResponse(license=result, success=True, message="License verified successfully")
659 + return VerifyLicenseResponse(license=result["data"]["license"], success=True, message="License verified successfully")
660 except Exception as e:
661 logger.error(e)
662 raise HTTPException(status_code=400, detail="License verification failed")
@@ -456,75 +680,108 @@ async def get_license_key(session: AsyncSession = Depends(get_db)) -> GetLicense
680 return GetLicenseResponse(license_key=license.license_key, success=True, message="License retrieved successfully")
681
682
683 +async def send_post_request(endpoint: str, data: Dict[str, Any] = None) -> Dict[str, Any]:
684 + """
685 + Sends a POST request to the Shuffle service.
686 +
687 + Args:
688 + endpoint (str): The endpoint to send the POST request to.
689 + data (Dict[str, Any]): The data to send with the POST request.
690 + connector_name (str, optional): The name of the connector to use. Defaults to "Shuffle".
691 +
692 + Returns:
693 + Dict[str, Any]: The response from the POST request.
694 + """
695 + logger.info(f"Sending POST request to {endpoint}")
696 +
697 + try:
698 + HEADERS = {
699 + "x-api-key": f"{os.getenv('COPILOT_API_KEY')}",
700 + "Content-Type": "application/json",
701 + "module-version": "1.0",
702 + }
703 + response = requests.post(
704 + f"https://license.socfortress.co/{endpoint}",
705 + headers=HEADERS,
706 + json=data,
707 + verify=False,
708 + )
709 +
710 + if response.status_code == 200:
711 + return {
712 + "data": response.json(),
713 + "success": True,
714 + "message": "Successfully retrieved data",
715 + }
716 + else:
717 + return {
718 + "success": False,
719 + "message": f"Failed to send POST request to {endpoint}",
720 + }
721 + except Exception as e:
722 + logger.error(f"Failed to send POST request to {endpoint} with error: {e}")
723 + raise HTTPException(
724 + status_code=500,
725 + detail=f"Failed to send POST request to {endpoint} with error: {e}",
726 + )
727 +
728 +
729 @license_router.get(
460 - "/get_license_features",
461 - response_model=GetLicenseFeaturesResponse,
462 - description="Get license features",
730 + "/is_feature_enabled/{feature_name}",
731 + response_model=IsFeatureEnabledResponse,
732 + description="Check if a feature is enabled in a license",
733 )
464 -async def get_license_features(session: AsyncSession = Depends(get_db)) -> GetLicenseFeaturesResponse:
734 +async def is_feature_enabled_route(feature_name: str, session: AsyncSession = Depends(get_db)) -> IsFeatureEnabledResponse:
735 """
466 - Get the features enabled in a license.
736 + Check if a feature is enabled in a license.
737
738 Args:
739 + feature_name (str): The feature name to check.
740 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
741
742 Returns:
472 - dict: A dictionary containing the features enabled in the license.
743 + bool: True if the feature is enabled, False otherwise.
744 """
474 - license = await get_license(session)
475 - try:
476 - license_details = LicenseResponse(**check_license(license).__dict__)
477 - features = {}
478 - for data_object in license_details.data_objects:
479 - features[data_object["Name"]] = data_object["IntValue"]
480 - return GetLicenseFeaturesResponse(
481 - features=[feature for feature, value in features.items() if value == 1],
745 + if await is_feature_enabled(feature_name, session):
746 + return IsFeatureEnabledResponse(
747 + enabled=True,
748 success=True,
483 - message="License features retrieved successfully",
749 + message="Feature is enabled",
750 + )
751 + else:
752 + return IsFeatureEnabledResponse(
753 + enabled=False,
754 + success=True,
755 + message="Feature is not enabled",
756 )
485 - except Exception as e:
486 - logger.error(e)
487 - raise HTTPException(status_code=400, detail="Failed to get license features")
757
758
490 -@license_router.post(
491 - "/add_feature/{feature_name}",
492 - description="Add a feature to a license",
759 +@license_router.get(
760 + "/get_license_features",
761 + response_model=GetLicenseFeaturesResponse,
762 + description="Get license features",
763 )
494 -async def add_feature_to_license(feature_name: str, session: AsyncSession = Depends(get_db)):
764 +async def get_license_features(session: AsyncSession = Depends(get_db)) -> GetLicenseFeaturesResponse:
765 """
496 - Add a feature to a license.
766 + Get the features enabled in a license.
767
768 Args:
499 - feature_name (str): The feature name to add.
769 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
770
771 Returns:
503 - LicenseVerificationResponse: A Pydantic model containing the verification status and message.
772 + dict: A dictionary containing the features enabled in the license.
773 """
505 - logger.info(f"Adding feature: {feature_name} to license")
506 - # Check if the feature name is valid
507 - feature_name = Feature.get_feature_name(feature_name)
508 - if feature_name is None:
509 - logger.error("Invalid feature name")
510 - raise HTTPException(status_code=400, detail="Invalid feature name")
774 + license = await get_license(session)
775 try:
512 - license = await get_license(session)
513 - logger.info(f"License: {license}")
514 - result, _ = Data.add_data_object_to_key(
515 - token=get_auth_token(),
516 - product_id=get_product_id(),
517 - key=license.license_key,
518 - name=feature_name,
519 - string_value=f"[{feature_name}]",
520 - check_for_duplicates=True,
521 - int_value=1,
776 + results = await send_post_request("license-features", data={"license_key": license.license_key})
777 + return GetLicenseFeaturesResponse(
778 + features=results["data"]["features"],
779 + success=results["success"],
780 + message=results["message"],
781 )
523 - logger.info(result)
524 - return result
782 except Exception as e:
783 logger.error(e)
527 - raise HTTPException(status_code=400, detail="Feature addition failed")
784 + raise HTTPException(status_code=400, detail="Failed to get license features")
785
786
787 @license_router.post(
@@ -547,7 +804,18 @@ async def replace_license_in_db(request: ReplaceLicenseRequest, session: AsyncSe
804 result = await session.execute(select(License))
805 license = result.scalars().first()
806 if not license:
550 - raise HTTPException(status_code=404, detail="No license found")
807 + # Verify the license key
808 + license_data = await send_post_request("verify-license", data={"license_key": request.license_key})
809 + if is_license_expired(license_data):
810 + raise HTTPException(status_code=400, detail="License is expired")
811 + # Create a new License object with the data from the dictionary
812 + license = License(
813 + license_key=license_data["data"]["license"]["key"],
814 + customer_name=license_data["data"]["license"]["customer"]["name"],
815 + customer_email=license_data["data"]["license"]["customer"]["email"],
816 + company_name=license_data["data"]["license"]["customer"]["companyName"],
817 + )
818 + session.add(license)
819 license.license_key = request.license_key
820 await session.commit()
821 return {"message": "License replaced successfully", "success": True}
@@ -573,6 +841,9 @@ def create_payload(request: ThreatIntelRegisterRequest) -> Dict[str, str]:
841
842
843 async def update_connector(response: ThreatIntelRegisterResponse, session: AsyncSession):
844 + """
845 + When Threat Intel is purchased, add the API key to the connector.
846 + """
847 await ConnectorServices.update_connector_by_id(
848 connector_id=10,
849 connector=UpdateConnector(
@@ -581,42 +852,3 @@ async def update_connector(response: ThreatIntelRegisterResponse, session: Async
852 ),
853 session=session,
854 )
584 -
585 -
586 -@license_router.post(
587 - "/register_to_threat_intel",
588 - description="Register to the SOCFortress Threat Intel Feed",
589 -)
590 -async def register_to_threat_intel(
591 - request: ThreatIntelRegisterRequest,
592 - session: AsyncSession = Depends(get_db),
593 -):
594 - """
595 - Register to the SOCFortress Threat Intel Feed.
596 -
597 - Args:
598 - request (ThreatIntelRegisterRequest): The request containing the customer name.
599 -
600 - Returns:
601 - ThreatIntelRegisterResponse: A Pydantic model containing the API key, success status, and message.
602 - """
603 - logger.info(f"Registering to the SOCFortress Threat Intel Feed: {request}")
604 - try:
605 - headers = create_headers(request)
606 - payload = create_payload(request)
607 - response = ThreatIntelRegisterResponse(
608 - **requests.post(
609 - request.registration_url,
610 - headers=headers,
611 - json=payload,
612 - ).json(),
613 - )
614 - await update_connector(response, session)
615 - return ThreatIntelRegisterResponse(
616 - api_key=response.api_key,
617 - success=response.success,
618 - message=response.message,
619 - )
620 - except Exception as e:
621 - logger.error(e)
622 - raise HTTPException(status_code=500, detail="Failed to register to the SOCFortress Threat Intel Feed")
backend/app/routers/carbonblack.py new
+15
@@ -0,0 +1,15 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.carbonblack.routes.provision import (
4 + integration_carbonblack_provision_scheduler_router,
5 +)
6 +
7 +# Instantiate the APIRouter
8 +router = APIRouter()
9 +
10 +# Include the Huntress Provision APIRouter
11 +router.include_router(
12 + integration_carbonblack_provision_scheduler_router,
13 + prefix="/carbonblack",
14 + tags=["carbonblack"],
15 +)
backend/app/routers/huntress.py
-8
@@ -1,6 +1,5 @@
1 from fastapi import APIRouter
2
3 -from app.integrations.huntress.routes.huntress import integration_huntress_router
3 from app.integrations.huntress.routes.provision import (
4 integration_huntress_provision_scheduler_router,
5 )
@@ -8,13 +7,6 @@ from app.integrations.huntress.routes.provision import (
7 # Instantiate the APIRouter
8 router = APIRouter()
9
11 -# Include the Huntress APIRouter
12 -router.include_router(
13 - integration_huntress_router,
14 - prefix="/huntress",
15 - tags=["huntress"],
16 -)
17 -
10 # Include the Huntress Provision APIRouter
11 router.include_router(
12 integration_huntress_provision_scheduler_router,
backend/app/routers/modules.py new
+18
@@ -0,0 +1,18 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.modules.routes.huntress import module_huntress_router
4 +from app.integrations.modules.routes.mimecast import module_mimecast_router
5 +
6 +router = APIRouter()
7 +
8 +router.include_router(
9 + module_huntress_router,
10 + prefix="/integrations/modules/huntress",
11 + tags=["Huntress"],
12 +)
13 +
14 +router.include_router(
15 + module_mimecast_router,
16 + prefix="/integrations/modules/mimecast",
17 + tags=["Mimecast"],
18 +)
backend/app/schedulers/scheduler.py
+4
@@ -7,6 +7,9 @@ from app.db.db_session import sync_engine
7 from app.schedulers.models.scheduler import CreateSchedulerRequest
8 from app.schedulers.models.scheduler import JobMetadata
9 from app.schedulers.services.agent_sync import agent_sync
10 +from app.schedulers.services.invoke_carbonblack import (
11 + invoke_carbonblack_integration_collect,
12 +)
13 from app.schedulers.services.invoke_huntress import invoke_huntress_integration_collect
14 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration
15 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp
@@ -134,6 +137,7 @@ def get_function_by_name(function_name: str):
137 "invoke_sap_siem_integration_brute_force_failed_logins_same_ip": invoke_sap_siem_integration_brute_force_failed_logins_same_ip,
138 "invoke_sap_siem_integration_successful_login_after_multiple_failed_logins": invoke_sap_siem_integration_successful_login_after_multiple_failed_logins,
139 "invoke_huntress_integration_collection": invoke_huntress_integration_collect,
140 + "invoke_carbonblack_integration_collection": invoke_carbonblack_integration_collect,
141 # Add other function mappings here
142 }
143 return function_map.get(
backend/app/schedulers/services/invoke_carbonblack.py new
+52
@@ -0,0 +1,52 @@
1 +from datetime import datetime
2 +
3 +from dotenv import load_dotenv
4 +from loguru import logger
5 +from sqlalchemy import select
6 +
7 +from app.db.db_session import get_db_session
8 +from app.db.db_session import get_sync_db_session
9 +from app.integrations.models.customer_integration_settings import CustomerIntegrations
10 +from app.integrations.modules.routes.carbonblack import collect_carbonblack_route
11 +from app.integrations.modules.schema.carbonblack import InvokeCarbonBlackRequest
12 +from app.integrations.modules.schema.carbonblack import InvokeCarbonBlackResponse
13 +from app.schedulers.models.scheduler import JobMetadata
14 +
15 +load_dotenv()
16 +
17 +
18 +async def invoke_carbonblack_integration_collect() -> InvokeCarbonBlackResponse:
19 + """
20 + Invokes the Huntress integration collection.
21 + """
22 + logger.info("Invoking Huntress integration collection.")
23 + customer_codes = []
24 + async with get_db_session() as session:
25 + stmt = select(CustomerIntegrations).where(
26 + CustomerIntegrations.integration_service_name == "CarbonBlack",
27 + )
28 + result = await session.execute(stmt)
29 + customer_codes = [row.customer_code for row in result.scalars()]
30 + logger.info(f"customer_codes: {customer_codes}")
31 + for customer_code in customer_codes:
32 + await collect_carbonblack_route(
33 + InvokeCarbonBlackRequest(
34 + customer_code=customer_code,
35 + integration_name="CarbonBlack",
36 + ),
37 + session,
38 + )
39 + # Close the session
40 + await session.close()
41 + with get_sync_db_session() as session:
42 + # Synchronous ORM operations
43 + job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_carbonblack_integration_collection").one_or_none()
44 + if job_metadata:
45 + job_metadata.last_success = datetime.utcnow()
46 + session.add(job_metadata)
47 + session.commit()
48 + else:
49 + # Handle the case where job_metadata does not exist
50 + print("JobMetadata for 'invoke_carbonblack_integration_collection' not found.")
51 +
52 + return InvokeCarbonBlackResponse(success=True, message="Carbonblack integration invoked.")
backend/app/schedulers/services/invoke_huntress.py
+3 -3
@@ -6,10 +6,10 @@ from sqlalchemy import select
6
7 from app.db.db_session import get_db_session
8 from app.db.db_session import get_sync_db_session
9 -from app.integrations.huntress.routes.huntress import collect_huntress_route
10 -from app.integrations.huntress.schema.huntress import InvokeHuntressRequest
11 -from app.integrations.huntress.schema.huntress import InvokeHuntressResponse
9 from app.integrations.models.customer_integration_settings import CustomerIntegrations
10 +from app.integrations.modules.routes.huntress import collect_huntress_route
11 +from app.integrations.modules.schema.huntress import InvokeHuntressRequest
12 +from app.integrations.modules.schema.huntress import InvokeHuntressResponse
13 from app.schedulers.models.scheduler import JobMetadata
14 from app.schedulers.utils.universal import get_scheduled_job_metadata
15
backend/copilot.py
+4
@@ -28,6 +28,7 @@ from app.routers import alert_creation
28 from app.routers import alert_creation_settings
29 from app.routers import ask_socfortress
30 from app.routers import auth
31 +from app.routers import carbonblack
32 from app.routers import connectors
33 from app.routers import cortex
34 from app.routers import customer_provisioning
@@ -43,6 +44,7 @@ from app.routers import integrations
44 from app.routers import license
45 from app.routers import logs
46 from app.routers import mimecast
47 +from app.routers import modules
48 from app.routers import monitoring_alert
49 from app.routers import office365
50 from app.routers import sap_siem
@@ -125,6 +127,8 @@ api_router.include_router(stack_provisioning.router)
127 api_router.include_router(active_response.router)
128 api_router.include_router(huntress.router)
129 api_router.include_router(license.router)
130 +api_router.include_router(modules.router)
131 +api_router.include_router(carbonblack.router)
132
133 # Include the APIRouter in the FastAPI app
134 app.include_router(api_router)
backend/requirements.txt
+1 -1
@@ -35,7 +35,7 @@ cybox==2.1.0.21
35 cycler==0.12.0
36 deepdiff==6.5.0
37 Deprecated==1.2.14
38 -dfir-iris-client==2.0.1
38 +dfir-iris-client==2.0.4
39 dnspython==2.4.2
40 dnstwist==20230918
41 drawsvg==2.3.0
frontend/package-lock.json
+82 -92
@@ -24,7 +24,7 @@
24 "detect-touch-device": "^1.1.6",
25 "echarts": "^5.5.0",
26 "file-saver": "^2.0.5",
27 - "jose": "^5.2.3",
27 + "jose": "^5.2.4",
28 "js-md5": "^0.8.3",
29 "lodash": "^4.17.21",
30 "markdown-it-highlightjs": "^4.0.1",
@@ -38,7 +38,7 @@
38 "vue": "^3.4.21",
39 "vue-advanced-cropper": "^2.8.8",
40 "vue-highlight-words": "^3.0.1",
41 - "vue-i18n": "^9.10.2",
41 + "vue-i18n": "^9.11.0",
42 "vue-router": "^4.3.0",
43 "vue-sjv": "^0.0.6",
44 "vue3-apexcharts": "^1.5.2",
@@ -48,7 +48,7 @@
48 "devDependencies": {
49 "@clack/prompts": "^0.7.0",
50 "@iconify/vue": "^4.1.1",
51 - "@rushstack/eslint-patch": "^1.9.0",
51 + "@rushstack/eslint-patch": "^1.10.1",
52 "@tsconfig/node18": "^18.2.4",
53 "@types/bytes": "^3.1.4",
54 "@types/file-saver": "^2.0.7",
@@ -58,9 +58,9 @@
58 "@types/inquirer": "^9.0.7",
59 "@types/jsdom": "^21.1.6",
60 "@types/lodash": "^4.17.0",
61 - "@types/markdown-it": "^13.0.7",
61 + "@types/markdown-it": "^14.0.0",
62 "@types/markdown-it-highlightjs": "^3.3.4",
63 - "@types/node": "^20.11.30",
63 + "@types/node": "^20.12.5",
64 "@types/validator": "^13.11.9",
65 "@vitejs/plugin-vue": "^5.0.4",
66 "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -69,10 +69,10 @@
69 "@vue/test-utils": "^2.4.5",
70 "@vue/tsconfig": "^0.5.1",
71 "autoprefixer": "^10.4.19",
72 - "cypress": "^13.7.1",
72 + "cypress": "^13.7.2",
73 "eslint": "^8.57.0",
74 "eslint-plugin-cypress": "^2.15.1",
75 - "eslint-plugin-vue": "^9.24.0",
75 + "eslint-plugin-vue": "^9.24.1",
76 "fs-extra": "^11.2.0",
77 "ip": "^2.0.1",
78 "jsdom": "^24.0.0",
@@ -81,18 +81,18 @@
81 "picocolors": "^1.0.0",
82 "postcss": "^8.4.38",
83 "prettier": "^3.2.5",
84 - "sass": "^1.72.0",
84 + "sass": "^1.74.1",
85 "start-server-and-test": "^2.0.3",
86 - "tailwind-config-viewer": "^1.7.3",
86 + "tailwind-config-viewer": "^2.0.1",
87 "tailwindcss": "^3.4.3",
88 "taze": "^0.13.3",
89 "unplugin-vue-components": "^0.26.0",
90 - "vite": "^5.2.6",
91 - "vite-bundle-analyzer": "^0.9.2",
90 + "vite": "^5.2.8",
91 + "vite-bundle-analyzer": "^0.9.3",
92 "vite-bundle-visualizer": "^1.1.0",
93 "vite-svg-loader": "^5.1.0",
94 "vitest": "^1.4.0",
95 - "vue-tsc": "^2.0.7"
95 + "vue-tsc": "^2.0.11"
96 },
97 "engines": {
98 "node": ">=18.0.0"
@@ -1340,12 +1340,12 @@
1340 }
1341 },
1342 "node_modules/@intlify/core-base": {
1343 - "version": "9.10.2",
1344 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.10.2.tgz",
1345 - "integrity": "sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg==",
1343 + "version": "9.11.0",
1344 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.11.0.tgz",
1345 + "integrity": "sha512-cveOqAstjLZIiyatcP/HrzrQ87cZI8ScPQna3yvoM8zjcjcIRK1MRvmxUNlPdg0rTNJMZw7rixPVM58O5aHVPA==",
1346 "dependencies": {
1347 - "@intlify/message-compiler": "9.10.2",
1348 - "@intlify/shared": "9.10.2"
1347 + "@intlify/message-compiler": "9.11.0",
1348 + "@intlify/shared": "9.11.0"
1349 },
1350 "engines": {
1351 "node": ">= 16"
@@ -1355,11 +1355,11 @@
1355 }
1356 },
1357 "node_modules/@intlify/message-compiler": {
1358 - "version": "9.10.2",
1359 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.10.2.tgz",
1360 - "integrity": "sha512-ntY/kfBwQRtX5Zh6wL8cSATujPzWW2ZQd1QwKyWwAy5fMqJyyixHMeovN4fmEyCqSu+hFfYOE63nU94evsy4YA==",
1358 + "version": "9.11.0",
1359 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.11.0.tgz",
1360 + "integrity": "sha512-x31Gl7cscnoI4UUY1yaIy8e7vVMVW1VVlTXZz4SIHKqoSEUkfmgqK8NAx1e7RcoHEbICR7uyCbud0ZL1s4OGXQ==",
1361 "dependencies": {
1362 - "@intlify/shared": "9.10.2",
1362 + "@intlify/shared": "9.11.0",
1363 "source-map-js": "^1.0.2"
1364 },
1365 "engines": {
@@ -1370,9 +1370,9 @@
1370 }
1371 },
1372 "node_modules/@intlify/shared": {
1373 - "version": "9.10.2",
1374 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.10.2.tgz",
1375 - "integrity": "sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q==",
1373 + "version": "9.11.0",
1374 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.11.0.tgz",
1375 + "integrity": "sha512-KHSNgi7sRjmSm7aD8QH8WFt9VfKaekJuJ473opbJlkGY3EDnDUU8ikIhG8PbasQbgNvbY3m3tWNGqk2omIdwMA==",
1376 "engines": {
1377 "node": ">= 16"
1378 },
@@ -2124,9 +2124,9 @@
2124 ]
2125 },
2126 "node_modules/@rushstack/eslint-patch": {
2127 - "version": "1.9.0",
2128 - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.9.0.tgz",
2129 - "integrity": "sha512-AAWymnpvHbGty1BmgbdfbqQDboXs6xN6h2yAacO4yKVyyUUBnpYkp+P9jjPrV9zrAGw7JVVriRtGOHPInnfjZQ==",
2127 + "version": "1.10.1",
2128 + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz",
2129 + "integrity": "sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg==",
2130 "dev": true
2131 },
2132 "node_modules/@sideway/address": {
@@ -2373,9 +2373,9 @@
2373 }
2374 },
2375 "node_modules/@types/markdown-it": {
2376 - "version": "13.0.7",
2377 - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-13.0.7.tgz",
2378 - "integrity": "sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==",
2376 + "version": "14.0.0",
2377 + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.0.0.tgz",
2378 + "integrity": "sha512-2rStaAqMaLQNfo9mg2HNlley75jUTAkZKqlk3pxDSgaFk44zd+CAVpczpoh6/RtOzfUtwpEyD6lsHWUfKbVSDg==",
2379 "dev": true,
2380 "dependencies": {
2381 "@types/linkify-it": "*",
@@ -2408,9 +2408,9 @@
2408 "dev": true
2409 },
2410 "node_modules/@types/node": {
2411 - "version": "20.11.30",
2412 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
2413 - "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
2411 + "version": "20.12.5",
2412 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.5.tgz",
2413 + "integrity": "sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==",
2414 "dev": true,
2415 "dependencies": {
2416 "undici-types": "~5.26.4"
@@ -2848,30 +2848,30 @@
2848 }
2849 },
2850 "node_modules/@volar/language-core": {
2851 - "version": "2.1.6",
2852 - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.6.tgz",
2853 - "integrity": "sha512-pAlMCGX/HatBSiDFMdMyqUshkbwWbLxpN/RL7HCQDOo2gYBE+uS+nanosLc1qR6pTQ/U8q00xt8bdrrAFPSC0A==",
2851 + "version": "2.2.0-alpha.6",
2852 + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.0-alpha.6.tgz",
2853 + "integrity": "sha512-GmT28LX2w4x82uuQqNN/P94VOCsZRHBbGcGe+5bFtA2hbIbH6f8tFdMfgXFtyhbft/pj6f3xl37xe+t+nomLIA==",
2854 "dev": true,
2855 "dependencies": {
2856 - "@volar/source-map": "2.1.6"
2856 + "@volar/source-map": "2.2.0-alpha.6"
2857 }
2858 },
2859 "node_modules/@volar/source-map": {
2860 - "version": "2.1.6",
2861 - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.6.tgz",
2862 - "integrity": "sha512-TeyH8pHHonRCHYI91J7fWUoxi0zWV8whZTVRlsWHSYfjm58Blalkf9LrZ+pj6OiverPTmrHRkBsG17ScQyWECw==",
2860 + "version": "2.2.0-alpha.6",
2861 + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.0-alpha.6.tgz",
2862 + "integrity": "sha512-EztD2zoUopETY+ZCUZAGUHKgj4gOkY/2WnaOS+RSTc56xm85miSA4qOBS8Lt1Ruu5vV52WIZKHW/R9PbjkZWFA==",
2863 "dev": true,
2864 "dependencies": {
2865 "muggle-string": "^0.4.0"
2866 }
2867 },
2868 "node_modules/@volar/typescript": {
2869 - "version": "2.1.6",
2870 - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.6.tgz",
2871 - "integrity": "sha512-JgPGhORHqXuyC3r6skPmPHIZj4LoMmGlYErFTuPNBq9Nhc9VTv7ctHY7A3jMN3ngKEfRrfnUcwXHztvdSQqNfw==",
2869 + "version": "2.2.0-alpha.6",
2870 + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.0-alpha.6.tgz",
2871 + "integrity": "sha512-wTr0jO3wVXQ9FjBbWE2iX8GgDoiHp1Nttsb+tKk5IeUUb6f1uOjyeIXuS4KfeMBpCufthRO2st2O2uatAs/UXQ==",
2872 "dev": true,
2873 "dependencies": {
2874 - "@volar/language-core": "2.1.6",
2874 + "@volar/language-core": "2.2.0-alpha.6",
2875 "path-browserify": "^1.0.1"
2876 }
2877 },
@@ -3204,12 +3204,12 @@
3204 }
3205 },
3206 "node_modules/@vue/language-core": {
3207 - "version": "2.0.7",
3208 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.7.tgz",
3209 - "integrity": "sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==",
3207 + "version": "2.0.11",
3208 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.11.tgz",
3209 + "integrity": "sha512-5ivg8Vem/yckzXI3L3n0mdKBPRcHSlsGt6/dpbEx42PcH3MIHAjSAJBYvENXeWJxv2ClQc8BS2mH1Ho2U7jZig==",
3210 "dev": true,
3211 "dependencies": {
3212 - "@volar/language-core": "~2.1.3",
3212 + "@volar/language-core": "~2.2.0-alpha.6",
3213 "@vue/compiler-dom": "^3.4.0",
3214 "@vue/shared": "^3.4.0",
3215 "computeds": "^0.0.1",
@@ -4661,9 +4661,9 @@
4661 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4662 },
4663 "node_modules/cypress": {
4664 - "version": "13.7.1",
4665 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.1.tgz",
4666 - "integrity": "sha512-4u/rpFNxOFCoFX/Z5h+uwlkBO4mWzAjveURi3vqdSu56HPvVdyGTxGw4XKGWt399Y1JwIn9E1L9uMXQpc0o55w==",
4664 + "version": "13.7.2",
4665 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.2.tgz",
4666 + "integrity": "sha512-FF5hFI5wlRIHY8urLZjJjj/YvfCBrRpglbZCLr/cYcL9MdDe0+5usa8kTIrDHthlEc9lwihbkb5dmwqBDNS2yw==",
4667 "dev": true,
4668 "hasInstallScript": true,
4669 "dependencies": {
@@ -5597,9 +5597,9 @@
5597 }
5598 },
5599 "node_modules/eslint-plugin-vue": {
5600 - "version": "9.24.0",
5601 - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.24.0.tgz",
5602 - "integrity": "sha512-9SkJMvF8NGMT9aQCwFc5rj8Wo1XWSMSHk36i7ZwdI614BU7sIOR28ZjuFPKp8YGymZN12BSEbiSwa7qikp+PBw==",
5600 + "version": "9.24.1",
5601 + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.24.1.tgz",
5602 + "integrity": "sha512-wk3SuwmS1pZdcuJlokGYEi/buDOwD6KltvhIZyOnpJ/378dcQ4zchu9PAMbbLAaydCz1iYc5AozszcOOgZIIOg==",
5603 "dependencies": {
5604 "@eslint-community/eslint-utils": "^4.4.0",
5605 "globals": "^13.24.0",
@@ -5614,7 +5614,7 @@
5614 "node": "^14.17.0 || >=16.0.0"
5615 },
5616 "peerDependencies": {
5617 - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
5617 + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0"
5618 }
5619 },
5620 "node_modules/eslint-scope": {
@@ -7309,9 +7309,9 @@
7309 }
7310 },
7311 "node_modules/jose": {
7312 - "version": "5.2.3",
7313 - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz",
7314 - "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==",
7312 + "version": "5.2.4",
7313 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz",
7314 + "integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==",
7315 "funding": {
7316 "url": "https://github.com/sponsors/panva"
7317 }
@@ -9085,15 +9085,6 @@
9085 "url": "https://github.com/sponsors/sindresorhus"
9086 }
9087 },
9088 - "node_modules/opener": {
9089 - "version": "1.5.2",
9090 - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
9091 - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
9092 - "dev": true,
9093 - "bin": {
9094 - "opener": "bin/opener-bin.js"
9095 - }
9096 - },
9088 "node_modules/optionator": {
9089 "version": "0.9.3",
9090 "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -10519,9 +10510,9 @@
10510 "dev": true
10511 },
10512 "node_modules/sass": {
10522 - "version": "1.72.0",
10523 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz",
10524 - "integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==",
10513 + "version": "1.74.1",
10514 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.74.1.tgz",
10515 + "integrity": "sha512-w0Z9p/rWZWelb88ISOLyvqTWGmtmu2QJICqDBGyNnfG4OUnPX9BBjjYIXUpXCMOOg5MQWNpqzt876la1fsTvUA==",
10516 "dev": true,
10517 "dependencies": {
10518 "chokidar": ">=3.0.0 <4.0.0",
@@ -11377,9 +11368,9 @@
11368 "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
11369 },
11370 "node_modules/tailwind-config-viewer": {
11380 - "version": "1.7.3",
11381 - "resolved": "https://registry.npmjs.org/tailwind-config-viewer/-/tailwind-config-viewer-1.7.3.tgz",
11382 - "integrity": "sha512-rgeFXe9vL4njtaSI1y2uUAD1aRx05RYHbReN72ARAVEVSlNmS0Zf46pj3/ORc3xQwLK/AzbaIs6UFcK7hJSIlA==",
11371 + "version": "2.0.1",
11372 + "resolved": "https://registry.npmjs.org/tailwind-config-viewer/-/tailwind-config-viewer-2.0.1.tgz",
11373 + "integrity": "sha512-0mfPRjxzKvQNW5YNh1EXhURV54ZtBvK4489tD8iosAVO8MZagC5BSdcl1i2b0tG+TiYIyEzBwDGQpE9vV/5gaA==",
11374 "dev": true,
11375 "dependencies": {
11376 "@koa/router": "^12.0.1",
@@ -11396,7 +11387,7 @@
11387 "tailwindcss-config-viewer": "cli/index.js"
11388 },
11389 "engines": {
11399 - "node": ">=8"
11390 + "node": ">=13"
11391 },
11392 "peerDependencies": {
11393 "tailwindcss": "1 || 2 || 2.0.1-compat || 3"
@@ -12300,13 +12291,13 @@
12291 }
12292 },
12293 "node_modules/vite": {
12303 - "version": "5.2.6",
12304 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
12305 - "integrity": "sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==",
12294 + "version": "5.2.8",
12295 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
12296 + "integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
12297 "dev": true,
12298 "dependencies": {
12299 "esbuild": "^0.20.1",
12309 - "postcss": "^8.4.36",
12300 + "postcss": "^8.4.38",
12301 "rollup": "^4.13.0"
12302 },
12303 "bin": {
@@ -12355,12 +12346,11 @@
12346 }
12347 },
12348 "node_modules/vite-bundle-analyzer": {
12358 - "version": "0.9.2",
12359 - "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.9.2.tgz",
12360 - "integrity": "sha512-BVnGn1JyqNSN2Tz4cPeM1Ks0w207ESvnxzBp5yhk6Z7utSkZdXfZqZolX6NlkiV6EaIRA1ha9vfC32AhxOg8kw==",
12349 + "version": "0.9.3",
12350 + "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.9.3.tgz",
12351 + "integrity": "sha512-gizc6zMOYXBcqJH8XBNj7uWO3Qxsd9KkY4VS/d4/GSiOnIvXsPBYvZ6bCcIG1dbO1erNJ7QqPGIH6Ki6UOKeAw==",
12352 "dev": true,
12353 "dependencies": {
12363 - "opener": "^1.5.2",
12354 "picocolors": "^1.0.0",
12355 "source-map": "^0.7.4"
12356 }
@@ -12721,12 +12711,12 @@
12711 }
12712 },
12713 "node_modules/vue-i18n": {
12724 - "version": "9.10.2",
12725 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.10.2.tgz",
12726 - "integrity": "sha512-ECJ8RIFd+3c1d3m1pctQ6ywG5Yj8Efy1oYoAKQ9neRdkLbuKLVeW4gaY5HPkD/9ssf1pOnUrmIFjx2/gkGxmEw==",
12714 + "version": "9.11.0",
12715 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.11.0.tgz",
12716 + "integrity": "sha512-vU4gY6lu8Pdfs9BgKGiDAJmFDf88cceR47KcSB0VW4xJzUrXR/7qwqM7A8dQ2nedhoIDxoOm5Ro4pFd2KvJqbA==",
12717 "dependencies": {
12728 - "@intlify/core-base": "9.10.2",
12729 - "@intlify/shared": "9.10.2",
12718 + "@intlify/core-base": "9.11.0",
12719 + "@intlify/shared": "9.11.0",
12720 "@vue/devtools-api": "^6.5.0"
12721 },
12722 "engines": {
@@ -12772,13 +12762,13 @@
12762 }
12763 },
12764 "node_modules/vue-tsc": {
12775 - "version": "2.0.7",
12776 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.7.tgz",
12777 - "integrity": "sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==",
12765 + "version": "2.0.11",
12766 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.11.tgz",
12767 + "integrity": "sha512-dl5MEU4VGZdQFGBnKfPpAfV3SQmBDWs9o4YhUPvDmwk+zmb/RprzFJK2sagR6EWazogZhXENvykd3wBXWS9kng==",
12768 "dev": true,
12769 "dependencies": {
12780 - "@volar/typescript": "~2.1.3",
12781 - "@vue/language-core": "2.0.7",
12770 + "@volar/typescript": "~2.2.0-alpha.6",
12771 + "@vue/language-core": "2.0.11",
12772 "semver": "^7.5.4"
12773 },
12774 "bin": {
frontend/package.json
+12 -12
@@ -49,7 +49,7 @@
49 "detect-touch-device": "^1.1.6",
50 "echarts": "^5.5.0",
51 "file-saver": "^2.0.5",
52 - "jose": "^5.2.3",
52 + "jose": "^5.2.4",
53 "js-md5": "^0.8.3",
54 "lodash": "^4.17.21",
55 "markdown-it-highlightjs": "^4.0.1",
@@ -63,7 +63,7 @@
63 "vue": "^3.4.21",
64 "vue-advanced-cropper": "^2.8.8",
65 "vue-highlight-words": "^3.0.1",
66 - "vue-i18n": "^9.10.2",
66 + "vue-i18n": "^9.11.0",
67 "vue-router": "^4.3.0",
68 "vue-sjv": "^0.0.6",
69 "vue3-apexcharts": "^1.5.2",
@@ -73,7 +73,7 @@
73 "devDependencies": {
74 "@clack/prompts": "^0.7.0",
75 "@iconify/vue": "^4.1.1",
76 - "@rushstack/eslint-patch": "^1.9.0",
76 + "@rushstack/eslint-patch": "^1.10.1",
77 "@tsconfig/node18": "^18.2.4",
78 "@types/bytes": "^3.1.4",
79 "@types/file-saver": "^2.0.7",
@@ -83,9 +83,9 @@
83 "@types/inquirer": "^9.0.7",
84 "@types/jsdom": "^21.1.6",
85 "@types/lodash": "^4.17.0",
86 - "@types/markdown-it": "^13.0.7",
86 + "@types/markdown-it": "^14.0.0",
87 "@types/markdown-it-highlightjs": "^3.3.4",
88 - "@types/node": "^20.11.30",
88 + "@types/node": "^20.12.5",
89 "@types/validator": "^13.11.9",
90 "@vitejs/plugin-vue": "^5.0.4",
91 "@vitejs/plugin-vue-jsx": "^3.1.0",
@@ -94,10 +94,10 @@
94 "@vue/test-utils": "^2.4.5",
95 "@vue/tsconfig": "^0.5.1",
96 "autoprefixer": "^10.4.19",
97 - "cypress": "^13.7.1",
97 + "cypress": "^13.7.2",
98 "eslint": "^8.57.0",
99 "eslint-plugin-cypress": "^2.15.1",
100 - "eslint-plugin-vue": "^9.24.0",
100 + "eslint-plugin-vue": "^9.24.1",
101 "fs-extra": "^11.2.0",
102 "ip": "^2.0.1",
103 "jsdom": "^24.0.0",
@@ -106,18 +106,18 @@
106 "picocolors": "^1.0.0",
107 "postcss": "^8.4.38",
108 "prettier": "^3.2.5",
109 - "sass": "^1.72.0",
109 + "sass": "^1.74.1",
110 "start-server-and-test": "^2.0.3",
111 - "tailwind-config-viewer": "^1.7.3",
111 + "tailwind-config-viewer": "^2.0.1",
112 "tailwindcss": "^3.4.3",
113 "taze": "^0.13.3",
114 "unplugin-vue-components": "^0.26.0",
115 - "vite": "^5.2.6",
116 - "vite-bundle-analyzer": "^0.9.2",
115 + "vite": "^5.2.8",
116 + "vite-bundle-analyzer": "^0.9.3",
117 "vite-bundle-visualizer": "^1.1.0",
118 "vite-svg-loader": "^5.1.0",
119 "vitest": "^1.4.0",
120 - "vue-tsc": "^2.0.7"
120 + "vue-tsc": "^2.0.11"
121 },
122 "engines": {
123 "node": ">=18.0.0"
frontend/src/api/license.ts
+36 -1
@@ -1,6 +1,13 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 import { HttpClient } from "./httpClient"
3 -import type { License, LicenseFeatures, LicenseKey } from "@/types/license.d"
3 +import type {
4 + CheckoutPayload,
5 + License,
6 + LicenseCheckoutSession,
7 + LicenseFeatures,
8 + LicenseKey,
9 + SubscriptionFeature
10 +} from "@/types/license.d"
11
12 export interface NewLicensePayload {
13 name: string
@@ -8,21 +15,48 @@ export interface NewLicensePayload {
15 companyName: string
16 }
17
18 +export interface CancelSubscriptionPayload {
19 + customer_email: string
20 + subscription_price_id: string
21 + feature_name: string
22 +}
23 +
24 export default {
25 getLicense() {
26 return HttpClient.get<FlaskBaseResponse & { license_key: LicenseKey }>(`/license/get_license`)
27 },
28 + getSubscriptionFeatures() {
29 + return HttpClient.get<FlaskBaseResponse & { features: SubscriptionFeature[] }>(`/license/subscription_features`)
30 + },
31 verifyLicense() {
32 return HttpClient.get<FlaskBaseResponse & { license: License }>(`/license/verify_license`)
33 },
34 getLicenseFeatures() {
35 return HttpClient.get<FlaskBaseResponse & { features: LicenseFeatures[] }>(`/license/get_license_features`)
36 },
37 + isFeatureEnabled(feature_name: LicenseFeatures) {
38 + return HttpClient.get<FlaskBaseResponse & { enabled: boolean }>(`/license/is_feature_enabled/${feature_name}`)
39 + },
40 replaceLicense(license_key: LicenseKey) {
41 return HttpClient.post<FlaskBaseResponse>(`/license/replace_license_in_db`, {
42 license_key
43 })
44 },
45 + createCheckoutSession(payload: CheckoutPayload) {
46 + return HttpClient.post<FlaskBaseResponse & { session: LicenseCheckoutSession }>(
47 + `/license/create_checkout_session`,
48 + payload
49 + )
50 + },
51 + retrieveLicenseByEmail(email: string) {
52 + return HttpClient.post<FlaskBaseResponse & { license_key: LicenseKey }>(`/license/retrieve_license_by_email`, {
53 + email
54 + })
55 + },
56 + cancelSubscription(payload: CancelSubscriptionPayload) {
57 + return HttpClient.post<FlaskBaseResponse>(`/license/cancel_subscription`, payload)
58 + },
59 + // TODO: remove, deprecated
60 extendLicense(period: number) {
61 return HttpClient.post<FlaskBaseResponse>(
62 `/license/extend_license`,
@@ -32,6 +66,7 @@ export default {
66 }
67 )
68 },
69 + // TODO: remove, deprecated
70 createLicense({ name, email, companyName }: NewLicensePayload) {
71 return HttpClient.post<FlaskBaseResponse>(`/license/create_new_key`, {
72 product_id: 24355,
frontend/src/assets/scss/helpers.scss
+21
@@ -69,6 +69,27 @@
69 }
70 }
71
72 +.overlay {
73 + background-color: rgba(var(--bg-body-rgb), 0.8);
74 + position: absolute;
75 + top: 0;
76 + left: 0;
77 + right: 0;
78 + bottom: 0;
79 + display: flex;
80 + align-items: center;
81 + justify-content: center;
82 + padding: 40px;
83 + text-align: center;
84 + font-size: 20px;
85 +
86 + .n-alert {
87 + width: 90vw;
88 + max-width: 500px;
89 + background-color: var(--bg-color);
90 + }
91 +}
92 +
93 .grid-auto-flow-200 {
94 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
95 grid-auto-flow: row dense;
frontend/src/components/AuthForm/index.vue
+1 -2
@@ -37,8 +37,7 @@ import Logo from "@/layouts/common/Logo.vue"
37 import { NButton } from "naive-ui"
38 import { ref, onBeforeMount, computed } from "vue"
39 import { useRouter } from "vue-router"
40 -
41 -export type FormType = "signin" | "signup" | "forgotpassword"
40 +import type { FormType } from "./types"
41
42 const props = defineProps<{
43 type?: FormType
frontend/src/components/AuthForm/types.d.ts new
+1
@@ -0,0 +1 @@
1 +export type FormType = "signin" | "signup" | "forgotpassword"
frontend/src/components/artifacts/CollectItem.vue
-1
@@ -51,7 +51,6 @@ const dFormats = useSettingsStore().dateFormat
51 onBeforeMount(() => {
52 for (const key in collect) {
53 const value = collect[key]
54 - console.log(key, value, typeof value)
54
55 const prop: Prop = {
56 key: "",
frontend/src/components/common/Badge.vue
+22 -2
@@ -3,7 +3,7 @@
3 :is="!!href ? 'a' : 'div'"
4 class="badge"
5 :href="href"
6 - :class="[type, color, { 'cursor-help': hintCursor, 'cursor-pointer': pointCursor }]"
6 + :class="[type, color, { 'cursor-help': hintCursor, 'cursor-pointer': pointCursor, fluid }]"
7 >
8 <span v-if="$slots.label || $slots.iconLeft || $slots.iconRight" class="flex items-center gap-2">
9 <slot name="iconLeft"></slot>
@@ -17,10 +17,11 @@
17 </template>
18
19 <script setup lang="ts">
20 -const { type, hintCursor, pointCursor, color, href } = defineProps<{
20 +const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
21 type?: "splitted" | "muted" | "active" | "cursor"
22 hintCursor?: boolean
23 pointCursor?: boolean
24 + fluid?: boolean
25 color?: "danger" | "warning"
26 href?: string
27 }>()
@@ -74,6 +75,8 @@ const { type, hintCursor, pointCursor, color, href } = defineProps<{
75 &:first-child {
76 border-right: var(--border-small-100);
77 background-color: var(--primary-005-color);
78 + line-height: 1.1;
79 + white-space: nowrap;
80 }
81 &:last-child {
82 font-family: var(--font-family-mono);
@@ -96,5 +99,22 @@ const { type, hintCursor, pointCursor, color, href } = defineProps<{
99 }
100 }
101 }
102 +
103 + &.fluid {
104 + min-height: 26px;
105 + height: unset;
106 +
107 + &.splitted {
108 + & > span {
109 + &:last-child {
110 + line-height: 1.1;
111 + padding-top: 5px;
112 + padding-bottom: 5px;
113 + display: flex;
114 + align-items: center;
115 + }
116 + }
117 + }
118 + }
119 }
120 </style>
frontend/src/components/common/KVCard.vue
+1
@@ -28,6 +28,7 @@
28 font-family: var(--font-family-mono);
29 height: 100%;
30 flex-grow: 1;
31 + word-break: break-word;
32 }
33 }
34 </style>
frontend/src/components/customers/provision/CustomerProvisionWizard.vue
+8
@@ -54,6 +54,13 @@
54 clearable
55 />
56 </n-form-item>
57 + <n-form-item label="DFIR-IRIS Username" path="dfir_iris_username" class="grow">
58 + <n-input
59 + v-model:value.trim="form.dfir_iris_username"
60 + placeholder="The Username of the API Key CoPilot uses to connect to DFIR-IRIS..."
61 + clearable
62 + />
63 + </n-form-item>
64 </div>
65
66 <div v-else-if="current === 2" class="px-7 flex flex-col gap-3">
@@ -437,6 +444,7 @@ function getClearForm(settings?: CustomerProvisioningDefaultSettings): CustomerP
444 customer_name: customerName.value,
445 customer_code: customerCode.value,
446 customer_grafana_org_name: "",
447 + dfir_iris_username: "",
448
449 // step 2
450 customer_index_name: "",
frontend/src/components/license/LicenseCheckoutResponse.vue new
+82
@@ -0,0 +1,82 @@
1 +<template>
2 + <n-card
3 + class="license-checkout-response"
4 + size="large"
5 + :class="type"
6 + content-class="flex flex-col items-center gap-5"
7 + >
8 + <template v-if="type === 'success'">
9 + <Icon :name="CheckIcon" class="text-success-color" :size="100" />
10 + <h1 class="text-center">Congratulations!</h1>
11 + <p class="text-center">Your checkout was successful, and your license will be updated soon.</p>
12 + <n-spin v-if="loadingLicense" :size="24" />
13 + <h4 v-if="license">
14 + {{ license }}
15 + </h4>
16 + </template>
17 + <template v-if="type === 'error'">
18 + <Icon :name="ErrorIcon" class="text-error-color" :size="100"></Icon>
19 + <h1 class="text-center">Checkout canceled</h1>
20 + </template>
21 + <div>
22 + <n-button @click="gotoLicense()">
23 + <template #icon>
24 + <Icon :name="LicenseIcon"></Icon>
25 + </template>
26 + View license
27 + </n-button>
28 + </div>
29 + </n-card>
30 +</template>
31 +
32 +<script setup lang="ts">
33 +import { onBeforeMount, ref, toRefs } from "vue"
34 +import Api from "@/api"
35 +import { NButton, NCard, NSpin, useMessage } from "naive-ui"
36 +import Icon from "@/components/common/Icon.vue"
37 +import type { LicenseKey } from "@/types/license"
38 +import { useRouter } from "vue-router"
39 +
40 +const props = defineProps<{ type: "success" | "error"; data?: { email?: string } }>()
41 +const { type, data } = toRefs(props)
42 +
43 +const ErrorIcon = "majesticons:exclamation-line"
44 +const LicenseIcon = "carbon:license"
45 +const CheckIcon = "carbon:checkmark-outline"
46 +const router = useRouter()
47 +const message = useMessage()
48 +const loadingLicense = ref(false)
49 +const license = ref<LicenseKey | null>(null)
50 +
51 +function getLicense(email: string) {
52 + loadingLicense.value = true
53 +
54 + Api.license
55 + .retrieveLicenseByEmail(email)
56 + .then(res => {
57 + if (res.data.success) {
58 + license.value = res.data?.license_key
59 + } else {
60 + message.warning(res.data?.message || "An error occurred. Please try again later.")
61 + }
62 + })
63 + .catch(err => {
64 + if (err.response.status !== 404) {
65 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
66 + }
67 + })
68 + .finally(() => {
69 + loadingLicense.value = false
70 + })
71 +}
72 +
73 +function gotoLicense() {
74 + router.push({ name: "License" })
75 +}
76 +
77 +onBeforeMount(() => {
78 + if (data.value?.email) {
79 + getLicense(data.value.email)
80 + }
81 +})
82 +</script>
frontend/src/components/license/LicenseCheckoutWizard.vue new
+287
@@ -0,0 +1,287 @@
1 +<template>
2 + <n-spin :show="loading" class="min-h-48">
3 + <n-empty :description="errorMessage" class="justify-center h-48" v-if="errorMessage">
4 + <template #icon><Icon :name="WarningIcon"></Icon></template>
5 + </n-empty>
6 + <template v-else>
7 + <template v-if="!selectedSubscription">
8 + <div class="list flex flex-col gap-2" v-if="availableSubscriptions.length">
9 + <SubscriptionCard
10 + v-for="subscription of availableSubscriptions"
11 + :key="subscription.id"
12 + :subscription="subscription"
13 + selectable
14 + embedded
15 + class="item-appear item-appear-bottom item-appear-005"
16 + @click="selectedSubscription = subscription"
17 + />
18 + </div>
19 + <template v-else>
20 + <n-empty
21 + description="Congratulations, you have already unlocked all available features"
22 + class="justify-center h-48"
23 + v-if="!loading"
24 + >
25 + <template #icon>
26 + <Icon :name="CheckIcon"></Icon>
27 + </template>
28 + </n-empty>
29 + </template>
30 + </template>
31 + <template v-else>
32 + <SubscriptionCard :subscription="selectedSubscription" embedded hide-details />
33 + <div class="checkout-form item-appear item-appear-bottom item-appear-005 mt-8">
34 + <n-spin :show="loadingLicense || loadingSession">
35 + <n-form :label-width="80" :model="checkoutForm" :rules="rules" ref="formRef">
36 + <div class="flex flex-col gap-1">
37 + <n-form-item label="Company Name" path="company_name">
38 + <n-input
39 + v-model:value.trim="checkoutForm.company_name"
40 + placeholder="Input Company Name..."
41 + clearable
42 + />
43 + </n-form-item>
44 + <n-form-item label="Email" path="customer_email">
45 + <n-input
46 + v-model:value.trim="checkoutForm.customer_email"
47 + placeholder="Input email..."
48 + clearable
49 + />
50 + </n-form-item>
51 + <div class="flex justify-end gap-4">
52 + <n-button quaternary @click="selectedSubscription = null">
53 + <template #icon>
54 + <Icon :name="ArrowLeftIcon"></Icon>
55 + </template>
56 + Back
57 + </n-button>
58 + <n-button
59 + type="success"
60 + :disabled="!isValid"
61 + @click="createCheckoutSession()"
62 + :loading="loadingSession"
63 + >
64 + <template #icon>
65 + <Icon :name="CartIcon"></Icon>
66 + </template>
67 + Checkout
68 + </n-button>
69 + </div>
70 + </div>
71 + </n-form>
72 + </n-spin>
73 + </div>
74 + </template>
75 + </template>
76 + </n-spin>
77 +</template>
78 +
79 +<script setup lang="ts">
80 +import {
81 + NSpin,
82 + NEmpty,
83 + NForm,
84 + NFormItem,
85 + NInput,
86 + NButton,
87 + useMessage,
88 + type FormItemRule,
89 + type FormRules
90 +} from "naive-ui"
91 +import Icon from "@/components/common/Icon.vue"
92 +import Api from "@/api"
93 +import { onBeforeMount, ref, toRefs } from "vue"
94 +import { computed } from "vue"
95 +import SubscriptionCard from "./SubscriptionCard.vue"
96 +import type { CheckoutPayload, License, LicenseCustomer, LicenseFeatures, SubscriptionFeature } from "@/types/license.d"
97 +import isEmail from "validator/es/lib/isEmail"
98 +import { watch } from "vue"
99 +
100 +const props = defineProps<{
101 + featuresData?: LicenseFeatures[]
102 + subscriptionsData?: SubscriptionFeature[]
103 +}>()
104 +const { featuresData, subscriptionsData } = toRefs(props)
105 +
106 +const WarningIcon = "carbon:warning-alt"
107 +const CartIcon = "carbon:shopping-cart"
108 +const CheckIcon = "carbon:checkmark-outline"
109 +const ArrowLeftIcon = "carbon:arrow-left"
110 +
111 +const message = useMessage()
112 +const loadingFeatures = ref(false)
113 +const loadingSubscriptions = ref(false)
114 +const loadingLicense = ref(false)
115 +const loadingSession = ref(false)
116 +const loading = computed(() => loadingFeatures.value || loadingSubscriptions.value)
117 +const selectedSubscription = ref<SubscriptionFeature | null>(null)
118 +const errorMessage = ref<string | null>(null)
119 +const checkoutForm = ref<CheckoutPayload>(getCheckoutForm())
120 +
121 +const license = ref<License | null>(null)
122 +const featuresLoaded = ref<LicenseFeatures[]>([])
123 +const subscriptionsLoaded = ref<SubscriptionFeature[]>([])
124 +const features = computed(() => featuresLoaded.value || featuresData?.value || [])
125 +const subscriptions = computed(() => subscriptionsLoaded.value || subscriptionsData?.value || [])
126 +const availableSubscriptions = computed<SubscriptionFeature[]>(() =>
127 + subscriptions.value.filter(o => !features.value.includes(o.name))
128 +)
129 +const isValid = computed(() => {
130 + if (!checkoutForm.value.company_name) {
131 + return false
132 + }
133 +
134 + if (!isEmail(checkoutForm.value.customer_email)) {
135 + return false
136 + }
137 +
138 + return true
139 +})
140 +
141 +const rules: FormRules = {
142 + company_name: {
143 + required: true,
144 + message: "Please input company name",
145 + trigger: ["input", "blur"]
146 + },
147 + customer_email: {
148 + required: true,
149 + trigger: ["input", "blur"],
150 + validator: (rule: FormItemRule, value: string) => {
151 + if (!value) {
152 + return new Error("Email is required")
153 + }
154 + if (!isEmail(value)) {
155 + return new Error("The email is not formatted correctly")
156 + }
157 + }
158 + }
159 +}
160 +
161 +watch(selectedSubscription, val => {
162 + if (val && !license.value) {
163 + getLicense()
164 + }
165 +})
166 +
167 +function getCheckoutForm(args?: {
168 + email?: string
169 + companyName?: string
170 + customer?: LicenseCustomer
171 + subscription?: SubscriptionFeature | null
172 +}): CheckoutPayload {
173 + const customerEmail = args?.email || args?.customer?.email || ""
174 + const companyName = args?.companyName || args?.customer?.companyName || ""
175 +
176 + return {
177 + feature_id: args?.subscription?.id || 0,
178 + cancel_url: `${location.origin}/license/cancel`,
179 + success_url: `${location.origin}/license/success?email=${customerEmail}`,
180 + customer_email: customerEmail,
181 + company_name: companyName
182 + }
183 +}
184 +
185 +function getLicenseFeatures() {
186 + loadingFeatures.value = true
187 +
188 + Api.license
189 + .getLicenseFeatures()
190 + .then(res => {
191 + if (res.data.success) {
192 + featuresLoaded.value = res.data?.features
193 + } else {
194 + message.warning(res.data?.message || "An error occurred. Please try again later.")
195 + }
196 + })
197 + .catch(err => {
198 + if (err.response.status !== 404) {
199 + errorMessage.value = "We're sorry, there was an issue loading your license"
200 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
201 + }
202 + })
203 + .finally(() => {
204 + loadingFeatures.value = false
205 + })
206 +}
207 +
208 +function getSubscriptionFeatures() {
209 + loadingSubscriptions.value = true
210 +
211 + Api.license
212 + .getSubscriptionFeatures()
213 + .then(res => {
214 + if (res.data.success) {
215 + subscriptionsLoaded.value = res.data?.features || []
216 + } else {
217 + message.warning(res.data?.message || "An error occurred. Please try again later.")
218 + }
219 + })
220 + .catch(err => {
221 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
222 + })
223 + .finally(() => {
224 + loadingSubscriptions.value = false
225 + })
226 +}
227 +
228 +function getLicense() {
229 + loadingLicense.value = true
230 +
231 + Api.license
232 + .verifyLicense()
233 + .then(res => {
234 + if (res.data.success) {
235 + license.value = res.data?.license
236 + checkoutForm.value = getCheckoutForm({ customer: license.value.customer })
237 + } else {
238 + message.warning(res.data?.message || "An error occurred. Please try again later.")
239 + }
240 + })
241 + .catch(err => {
242 + if (err.response.status !== 404) {
243 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
244 + }
245 + })
246 + .finally(() => {
247 + loadingLicense.value = false
248 + })
249 +}
250 +
251 +function createCheckoutSession() {
252 + loadingSession.value = true
253 +
254 + const payload = getCheckoutForm({
255 + email: checkoutForm.value.customer_email,
256 + companyName: checkoutForm.value.company_name,
257 + subscription: selectedSubscription.value
258 + })
259 +
260 + Api.license
261 + .createCheckoutSession(payload)
262 + .then(res => {
263 + if (res.data.success) {
264 + window.location.href = res.data.session.url
265 + } else {
266 + message.warning(res.data?.message || "An error occurred. Please try again later.")
267 + }
268 + })
269 + .catch(err => {
270 + if (err.response.status !== 404) {
271 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
272 + }
273 + })
274 + .finally(() => {
275 + loadingSession.value = false
276 + })
277 +}
278 +
279 +onBeforeMount(() => {
280 + if (!features.value.length) {
281 + getLicenseFeatures()
282 + }
283 + if (!subscriptions.value.length) {
284 + getSubscriptionFeatures()
285 + }
286 +})
287 +</script>
frontend/src/components/license/LicenseDetails.vue new
+190
@@ -0,0 +1,190 @@
1 +<template>
2 + <n-spin :show="loading" content-class=" grow flex flex-col" class="flex flex-col overflow-hidden">
3 + <div v-if="license" class="flex flex-col gap-4">
4 + <KVCard v-if="!hideKey" class="!basis-auto">
5 + <template #key>
6 + <span class="flex gap-3 items-center">
7 + <Icon :name="KeyIcon" :size="14"></Icon>
8 + <span>Key</span>
9 + </span>
10 + </template>
11 + <template #value>{{ license.key }}</template>
12 + </KVCard>
13 + <KVCard v-if="!hideFeatures" class="!basis-auto">
14 + <template #key>
15 + <span class="flex gap-3 items-center">
16 + <Icon :name="FeaturesIcon" :size="14"></Icon>
17 + <span>Features</span>
18 + </span>
19 + </template>
20 + <template #value>
21 + <div v-if="features.length" class="grid gap-2 grid-auto-flow-200">
22 + <KVCard v-for="feature of features" :key="feature">
23 + <template #value>
24 + <span class="flex gap-3 items-center">
25 + <Icon :name="CheckIcon" :size="14" class="text-primary-color"></Icon>
26 + <span>{{ feature }}</span>
27 + </span>
28 + </template>
29 + </KVCard>
30 + </div>
31 + <template v-else>No feature enabled</template>
32 + </template>
33 + </KVCard>
34 + <KVCard class="!basis-auto">
35 + <template #key>
36 + <span class="flex gap-3 items-center">
37 + <Icon :name="ExpiresIcon" :size="14"></Icon>
38 + <span>Expires</span>
39 + </span>
40 + </template>
41 + <template #value>
42 + {{ expiresText }}
43 + <span class="text-secondary-color">({{ periodText }})</span>
44 + </template>
45 + </KVCard>
46 + <KVCard class="!basis-auto">
47 + <template #key>
48 + <span class="flex gap-3 items-center">
49 + <Icon :name="CustomerIcon" :size="14"></Icon>
50 + <span>Customer</span>
51 + </span>
52 + </template>
53 + <template #value>
54 + <div class="flex flex-wrap gap-2">
55 + <Badge type="splitted" v-for="(value, key) of license.customer" :key="key" fluid>
56 + <template #label>{{ sanitizeKey(key) }}</template>
57 + <template #value>
58 + <template v-if="key === 'created'">
59 + {{ formatDate(value, dFormats.datetime) }}
60 + </template>
61 + <template v-else>{{ value ?? "-" }}</template>
62 + </template>
63 + </Badge>
64 + </div>
65 + </template>
66 + </KVCard>
67 + </div>
68 + </n-spin>
69 +</template>
70 +
71 +<script setup lang="ts">
72 +import { NSpin, useMessage } from "naive-ui"
73 +import Icon from "@/components/common/Icon.vue"
74 +import Api from "@/api"
75 +import { onBeforeMount, onMounted, ref, toRefs, computed } from "vue"
76 +import { type LicenseFeatures, type License } from "@/types/license.d"
77 +import { formatDate } from "@/utils"
78 +import { useSettingsStore } from "@/stores/settings"
79 +import _startCase from "lodash/startCase"
80 +import Badge from "@/components/common/Badge.vue"
81 +import KVCard from "@/components/common/KVCard.vue"
82 +
83 +const emit = defineEmits<{
84 + (e: "licenseLoaded", value: License): void
85 + (
86 + e: "mounted",
87 + value: {
88 + reload: () => void
89 + }
90 + ): void
91 +}>()
92 +
93 +const props = defineProps<{
94 + licenseData?: License
95 + featuresData?: LicenseFeatures[]
96 + hideKey?: boolean
97 + hideFeatures?: boolean
98 +}>()
99 +const { licenseData, featuresData, hideKey, hideFeatures } = toRefs(props)
100 +
101 +const KeyIcon = "ph:key"
102 +const ExpiresIcon = "ph:calendar-blank"
103 +const CustomerIcon = "carbon:user"
104 +const CheckIcon = "carbon:checkmark-outline"
105 +const FeaturesIcon = "material-symbols:checklist"
106 +
107 +const message = useMessage()
108 +const loadingLicense = ref(false)
109 +const loadingFeatures = ref(false)
110 +const dFormats = useSettingsStore().dateFormat
111 +
112 +const licenseLoaded = ref<License | null>(null)
113 +const featuresLoaded = ref<LicenseFeatures[]>([])
114 +const license = computed(() => licenseLoaded.value || licenseData?.value || null)
115 +const features = computed(() => featuresLoaded.value || featuresData?.value || [])
116 +const expiresText = computed(() => (license.value ? formatDate(license.value.expires, dFormats.datetime) : ""))
117 +const periodText = computed(() =>
118 + license.value ? `${license.value.period} Day${license.value.period === 1 ? "" : "s"}` : ""
119 +)
120 +
121 +const loading = computed(() => loadingLicense.value || loadingFeatures.value)
122 +
123 +function getLicense() {
124 + loadingLicense.value = true
125 +
126 + Api.license
127 + .verifyLicense()
128 + .then(res => {
129 + if (res.data.success) {
130 + licenseLoaded.value = res.data?.license
131 + emit("licenseLoaded", licenseLoaded.value)
132 + } else {
133 + message.warning(res.data?.message || "An error occurred. Please try again later.")
134 + }
135 + })
136 + .catch(err => {
137 + if (err.response.status !== 404) {
138 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
139 + }
140 + })
141 + .finally(() => {
142 + loadingLicense.value = false
143 + })
144 +}
145 +
146 +function getLicenseFeatures() {
147 + loadingFeatures.value = true
148 +
149 + Api.license
150 + .getLicenseFeatures()
151 + .then(res => {
152 + if (res.data.success) {
153 + featuresLoaded.value = res.data?.features
154 + } else {
155 + message.warning(res.data?.message || "An error occurred. Please try again later.")
156 + }
157 + })
158 + .catch(err => {
159 + if (err.response.status !== 404) {
160 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
161 + }
162 + })
163 + .finally(() => {
164 + loadingFeatures.value = false
165 + })
166 +}
167 +
168 +function load() {
169 + if (!license.value) {
170 + getLicense()
171 + }
172 + if (!hideFeatures.value && !features.value.length) {
173 + getLicenseFeatures()
174 + }
175 +}
176 +
177 +function sanitizeKey(text: string) {
178 + return _startCase(text).toLowerCase()
179 +}
180 +
181 +onBeforeMount(() => {
182 + load()
183 +})
184 +
185 +onMounted(() => {
186 + emit("mounted", {
187 + reload: load
188 + })
189 +})
190 +</script>
frontend/src/components/license/LicenseFeatureOverlay.vue new
+62
@@ -0,0 +1,62 @@
1 +<template>
2 + <div class="overlay feature-overlay" v-if="showBanner">
3 + <n-alert title="Feature required">
4 + <template #icon>
5 + <Icon :name="AlertIcon" :size="18"></Icon>
6 + </template>
7 + <div class="flex flex-col gap-4">
8 + <div>
9 + It seems that the feature you are looking for is currently unavailable. To unlock and use it, you
10 + need to enable this feature. You can manage your features from the License page.
11 + </div>
12 + <div class="flex justify-end">
13 + <n-button @click="gotoLicense()">
14 + <template #icon>
15 + <Icon :name="LicenseIcon"></Icon>
16 + </template>
17 + View license
18 + </n-button>
19 + </div>
20 + </div>
21 + </n-alert>
22 + </div>
23 +</template>
24 +
25 +<script setup lang="ts">
26 +import { ref } from "vue"
27 +import { NAlert, NButton } from "naive-ui"
28 +import Icon from "@/components/common/Icon.vue"
29 +import { useRouter } from "vue-router"
30 +import Api from "@/api"
31 +import { onBeforeMount } from "vue"
32 +import type { LicenseFeatures } from "@/types/license"
33 +
34 +const { feature } = defineProps<{ feature: LicenseFeatures }>()
35 +
36 +const LicenseIcon = "carbon:license"
37 +const AlertIcon = "mdi:alert-outline"
38 +
39 +const router = useRouter()
40 +const showBanner = ref(false)
41 +
42 +function gotoLicense() {
43 + router.push({ name: "License" })
44 +}
45 +
46 +function checkFeature(feature: LicenseFeatures) {
47 + Api.license
48 + .isFeatureEnabled(feature)
49 + .then(res => {
50 + if (!res.data.success) {
51 + showBanner.value = true
52 + }
53 + })
54 + .catch(() => {
55 + showBanner.value = true
56 + })
57 +}
58 +
59 +onBeforeMount(() => {
60 + checkFeature(feature)
61 +})
62 +</script>
frontend/src/components/license/LicenseFeatures.vue new
+281
@@ -0,0 +1,281 @@
1 +<template>
2 + <div class="license-features" :class="{ loading }">
3 + <div class="license-features-box flex items-center justify-center">
4 + <n-spin :show="loading" class="h-full w-full" content-class="h-full">
5 + <div class="wrapper h-full flex flex-col gap-4" v-if="!loading">
6 + <div class="flex justify-between items-center gap-4">
7 + <h3>
8 + {{ features.length ? "Your features" : "Unlock features" }}
9 + </h3>
10 + <n-popover class="max-w-80" trigger="hover" v-if="features.length">
11 + <template #trigger>
12 + <Icon :name="InfoIcon" :size="20" class="cursor-help"></Icon>
13 + </template>
14 + To unsubscribe, click on the feature you wish to remove, and then click on the "Unsubscribe"
15 + button.
16 + </n-popover>
17 + </div>
18 + <div class="grow overflow-hidden">
19 + <n-scrollbar v-if="!loading">
20 + <div class="features-list flex flex-col gap-2" v-if="license">
21 + <template v-if="activeSubscriptions.length">
22 + <SubscriptionCard
23 + v-for="subscription of activeSubscriptions"
24 + :key="subscription.id"
25 + :subscription="subscription"
26 + :licenseData="licenseData"
27 + embedded
28 + showDeleteOnDialog
29 + @deleted="load()"
30 + />
31 + </template>
32 + <n-empty description="No features unlocked" class="justify-center h-48" v-else>
33 + <template #icon>
34 + <Icon :name="NoFeaturesIcon"></Icon>
35 + </template>
36 + </n-empty>
37 + </div>
38 + <LicenseCheckoutWizard v-else :features-data="features" />
39 + </n-scrollbar>
40 + </div>
41 + <div class="cta-section" v-if="license">
42 + <n-button type="primary" @click="openCheckout()" class="!w-full" size="large">
43 + <template #icon>
44 + <Icon :name="ExtendIcon"></Icon>
45 + </template>
46 + Add feature
47 + </n-button>
48 + </div>
49 + </div>
50 + </n-spin>
51 + </div>
52 +
53 + <div class="footer mt-3" v-if="!hideKey && !loading">
54 + <template v-if="license">
55 + <span class="cursor-pointer" @click="showLicenseDetails = true">
56 + Your license:
57 + <strong>{{ license }}</strong>
58 + <Icon :name="InfoIcon" :size="14" class="relative top-0.5 ml-1"></Icon>
59 + </span>
60 + </template>
61 + <template v-else>
62 + <span class="cursor-pointer" @click="showLicenseUpload = true">
63 + If you possess a license, you may load it by clicking here
64 + </span>
65 + </template>
66 + </div>
67 +
68 + <n-modal
69 + v-model:show="showCheckoutForm"
70 + preset="card"
71 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
72 + title="Add feature"
73 + :bordered="false"
74 + content-class="flex flex-col"
75 + segmented
76 + >
77 + <LicenseCheckoutWizard :features-data="features" :subscriptions-data="subscriptions" />
78 + </n-modal>
79 +
80 + <n-modal
81 + v-model:show="showLicenseDetails"
82 + preset="card"
83 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
84 + title="License details"
85 + :bordered="false"
86 + content-class="flex flex-col"
87 + segmented
88 + >
89 + <LicenseDetails :features-data="features" hide-features v-if="license" />
90 + </n-modal>
91 +
92 + <n-modal
93 + v-model:show="showLicenseUpload"
94 + preset="card"
95 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
96 + title="Upload your license"
97 + :bordered="false"
98 + content-class="flex flex-col"
99 + segmented
100 + >
101 + <LicenseLoadForm @uploaded="licenseUploaded()" />
102 + </n-modal>
103 + </div>
104 +</template>
105 +
106 +<script setup lang="ts">
107 +import { NScrollbar, NSpin, NModal, NButton, NEmpty, NPopover, useMessage } from "naive-ui"
108 +import Icon from "@/components/common/Icon.vue"
109 +import Api from "@/api"
110 +import { onBeforeMount, onMounted, ref, toRefs, computed } from "vue"
111 +import { type License, type LicenseFeatures, type LicenseKey, type SubscriptionFeature } from "@/types/license.d"
112 +import SubscriptionCard from "./SubscriptionCard.vue"
113 +import LicenseCheckoutWizard from "./LicenseCheckoutWizard.vue"
114 +import LicenseLoadForm from "./LicenseLoadForm.vue"
115 +import LicenseDetails from "./LicenseDetails.vue"
116 +
117 +const emit = defineEmits<{
118 + (e: "licenseKeyLoaded", value: LicenseKey): void
119 + (
120 + e: "mounted",
121 + value: {
122 + reload: () => void
123 + }
124 + ): void
125 +}>()
126 +
127 +const props = defineProps<{
128 + hideKey?: boolean
129 + licenseData?: License
130 +}>()
131 +const { hideKey, licenseData } = toRefs(props)
132 +
133 +const InfoIcon = "carbon:information"
134 +const ExtendIcon = "carbon:intent-request-create"
135 +const NoFeaturesIcon = "carbon:intent-request-uninstall"
136 +
137 +const message = useMessage()
138 +const showCheckoutForm = ref(false)
139 +const showLicenseDetails = ref(false)
140 +const showLicenseUpload = ref(false)
141 +const loadingLicense = ref(false)
142 +const loadingFeatures = ref(false)
143 +const loadingSubscriptions = ref(false)
144 +
145 +const license = ref<LicenseKey | null>(null)
146 +const features = ref<LicenseFeatures[]>([])
147 +const subscriptions = ref<SubscriptionFeature[]>([])
148 +
149 +const loading = computed(() => loadingLicense.value || loadingFeatures.value || loadingSubscriptions.value)
150 +
151 +const activeSubscriptions = computed<SubscriptionFeature[]>(() =>
152 + features.value.map(f => subscriptions.value.find(s => s.name === f) as SubscriptionFeature).filter(o => !!o)
153 +)
154 +
155 +function getLicense() {
156 + loadingLicense.value = true
157 +
158 + Api.license
159 + .getLicense()
160 + .then(res => {
161 + if (res.data.success) {
162 + license.value = res.data?.license_key
163 + if (license.value) {
164 + emit("licenseKeyLoaded", license.value)
165 + }
166 + } else {
167 + message.warning(res.data?.message || "An error occurred. Please try again later.")
168 + }
169 + })
170 + .catch(err => {
171 + if (err.response.status !== 404) {
172 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
173 + }
174 + })
175 + .finally(() => {
176 + loadingLicense.value = false
177 + })
178 +}
179 +
180 +function getLicenseFeatures() {
181 + loadingFeatures.value = true
182 +
183 + Api.license
184 + .getLicenseFeatures()
185 + .then(res => {
186 + if (res.data.success) {
187 + features.value = res.data?.features
188 + if (features.value.length) {
189 + getSubscriptionFeatures()
190 + }
191 + } else {
192 + message.warning(res.data?.message || "An error occurred. Please try again later.")
193 + }
194 + })
195 + .catch(err => {
196 + if (err.response.status !== 404) {
197 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
198 + }
199 + })
200 + .finally(() => {
201 + loadingFeatures.value = false
202 + })
203 +}
204 +
205 +function getSubscriptionFeatures() {
206 + loadingSubscriptions.value = true
207 +
208 + Api.license
209 + .getSubscriptionFeatures()
210 + .then(res => {
211 + if (res.data.success) {
212 + subscriptions.value = res.data?.features || []
213 + } else {
214 + message.warning(res.data?.message || "An error occurred. Please try again later.")
215 + }
216 + })
217 + .catch(err => {
218 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
219 + })
220 + .finally(() => {
221 + loadingSubscriptions.value = false
222 + })
223 +}
224 +
225 +function openCheckout() {
226 + showCheckoutForm.value = true
227 +}
228 +
229 +function licenseUploaded() {
230 + showLicenseUpload.value = false
231 + load()
232 +}
233 +
234 +function load() {
235 + getLicense()
236 + getLicenseFeatures()
237 +}
238 +
239 +onBeforeMount(() => {
240 + load()
241 +})
242 +
243 +onMounted(() => {
244 + emit("mounted", {
245 + reload: load
246 + })
247 +})
248 +</script>
249 +
250 +<style lang="scss" scoped>
251 +.license-features {
252 + display: flex;
253 + flex-direction: column;
254 + overflow: hidden;
255 +
256 + &.loading {
257 + min-height: 300px;
258 + min-width: 300px;
259 + }
260 +
261 + .license-features-box {
262 + background-color: var(--bg-color);
263 + border-radius: var(--border-radius);
264 + border: var(--border-small-100);
265 + padding: 18px;
266 + flex-grow: 1;
267 + overflow: hidden;
268 + }
269 + .footer {
270 + width: 100%;
271 + text-align: center;
272 + font-size: 12px;
273 +
274 + .cursor-pointer {
275 + &:hover {
276 + color: var(--primary-color);
277 + }
278 + }
279 + }
280 +}
281 +</style>
frontend/src/components/license/LicenseLoadForm.vue new
+58
@@ -0,0 +1,58 @@
1 +<template>
2 + <n-spin :show="loading" content-class="flex flex-col gap-4">
3 + <n-input v-model:value="licenseKey" placeholder="Insert your license" clearable />
4 + <div class="flex justify-end">
5 + <n-button type="success" :loading="loadingReplace" :disabled="!licenseKey" @click="replaceLicense()">
6 + <template #icon>
7 + <Icon :name="LicenseIcon"></Icon>
8 + </template>
9 + Load License
10 + </n-button>
11 + </div>
12 + </n-spin>
13 +</template>
14 +
15 +<script setup lang="ts">
16 +import { NInput, NButton, NSpin, useMessage } from "naive-ui"
17 +import Icon from "@/components/common/Icon.vue"
18 +import Api from "@/api"
19 +import { ref } from "vue"
20 +import { computed } from "vue"
21 +import type { LicenseKey } from "@/types/license.d"
22 +
23 +const emit = defineEmits<{
24 + (e: "uploaded"): void
25 +}>()
26 +
27 +const LicenseIcon = "carbon:license"
28 +
29 +const message = useMessage()
30 +const loadingReplace = ref(false)
31 +const licenseKey = ref<LicenseKey | "">("")
32 +const loading = computed(() => loadingReplace.value)
33 +
34 +function replaceLicense() {
35 + if (!licenseKey.value) {
36 + return
37 + }
38 +
39 + loadingReplace.value = true
40 +
41 + Api.license
42 + .replaceLicense(licenseKey.value)
43 + .then(res => {
44 + if (res.data.success) {
45 + message.success(res.data?.message || "License replaced successfully")
46 + emit("uploaded")
47 + } else {
48 + message.warning(res.data?.message || "An error occurred. Please try again later.")
49 + }
50 + })
51 + .catch(err => {
52 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
53 + })
54 + .finally(() => {
55 + loadingReplace.value = false
56 + })
57 +}
58 +</script>
frontend/src/components/license/LicenseViewer.vue
+51 -159
@@ -1,180 +1,72 @@
1 <template>
2 - <n-spin :show="loading">
3 - <p class="mb-2" v-if="license">license details:</p>
4 - <div class="license-box flex flex-col gap-7" v-if="license">
5 - <div class="section" v-if="!hideKey">
6 - <div class="label">
7 - <Icon :name="KeyIcon" :size="14"></Icon>
8 - Key:
9 - </div>
10 - <div class="value">{{ license.key }}</div>
11 - </div>
12 - <div class="section">
13 - <div class="label">
14 - <Icon :name="ExpiresIcon" :size="14"></Icon>
15 - Expires:
16 - </div>
17 - <div class="value">{{ expiresText }}</div>
18 - </div>
19 - <div class="section">
20 - <div class="label">
21 - <Icon :name="PeriodIcon" :size="14"></Icon>
22 - Period:
23 - </div>
24 - <div class="value">{{ periodText }}</div>
25 - </div>
26 - <div class="section">
27 - <div class="label">
28 - <Icon :name="CustomerIcon" :size="14"></Icon>
29 - Customer:
30 - </div>
31 - <div class="value grid gap-2 grid-auto-flow-200">
32 - <KVCard v-for="(value, key) of license.customer" :key="key">
33 - <template #key>{{ key }}</template>
34 - <template #value>
35 - <template v-if="key === 'Created'">
36 - {{ formatDate(value, dFormats.datetime) }}
37 - </template>
38 - <template v-else>{{ value ?? "-" }}</template>
39 - </template>
40 - </KVCard>
41 - </div>
42 - </div>
43 - <div class="section">
44 - <div class="label">
45 - <Icon :name="FeaturesIcon" :size="14"></Icon>
46 - Features:
47 - </div>
48 - <div class="value">{{ featuresText || "No feature enabled" }}</div>
49 - </div>
2 + <div class="license-viewer flex justify-center items-center gap-4" :class="{ 'has-side': !!key }">
3 + <div class="main-box">
4 + <LicenseFeatures
5 + @license-key-loaded="licenseKeyLoaded"
6 + :hide-key="!!key"
7 + :license-data="details"
8 + class="h-full"
9 + :class="{ 'mt-8': !key }"
10 + />
11 </div>
51 - </n-spin>
12 + <div class="side-box">
13 + <n-scrollbar style="max-width: 100%">
14 + <LicenseDetails hide-features @license-loaded="licenseLoaded" v-if="key" class="min-h-full" />
15 + </n-scrollbar>
16 + </div>
17 + </div>
18 </template>
19
20 <script setup lang="ts">
55 -import { NSpin, useMessage } from "naive-ui"
56 -import Icon from "@/components/common/Icon.vue"
57 -import Api from "@/api"
58 -import { onBeforeMount, onMounted, ref } from "vue"
59 -import { computed } from "vue"
60 -import { LicenseFeatures, type License } from "@/types/license.d"
61 -import { formatDate } from "@/utils"
62 -import { useSettingsStore } from "@/stores/settings"
63 -import KVCard from "@/components/common/KVCard.vue"
21 +import { ref } from "vue"
22 +import { NScrollbar } from "naive-ui"
23 +import LicenseFeatures from "./LicenseFeatures.vue"
24 +import LicenseDetails from "./LicenseDetails.vue"
25 +import type { License, LicenseKey } from "@/types/license"
26
27 const emit = defineEmits<{
66 - (
67 - e: "mounted",
68 - value: {
69 - reload: () => void
70 - }
71 - ): void
28 + (e: "licenseKeyLoaded", value: LicenseKey): void
29 }>()
30
74 -const { hideKey } = defineProps<{ hideKey?: boolean }>()
75 -
76 -const KeyIcon = "ph:key"
77 -const ExpiresIcon = "ph:calendar-blank"
78 -const PeriodIcon = "majesticons:clock-line"
79 -const CustomerIcon = "carbon:user"
80 -const FeaturesIcon = "material-symbols:checklist"
81 -
82 -const message = useMessage()
83 -const loadingLicense = ref(false)
84 -const loadingFeatures = ref(false)
85 -const dFormats = useSettingsStore().dateFormat
86 -
87 -const license = ref<License | null>(null)
88 -const features = ref<LicenseFeatures[]>([])
89 -const expiresText = computed(() => (license.value ? formatDate(license.value.expires, dFormats.datetime) : ""))
90 -const periodText = computed(() =>
91 - license.value ? `${license.value.period} Day${license.value.period === 1 ? "" : "s"}` : ""
92 -)
93 -const featuresText = computed(() => features.value.join(", "))
94 -
95 -const loading = computed(() => loadingLicense.value || loadingFeatures.value)
96 -
97 -function getLicense() {
98 - loadingLicense.value = true
99 -
100 - Api.license
101 - .verifyLicense()
102 - .then(res => {
103 - if (res.data.success) {
104 - license.value = res.data?.license
105 - } else {
106 - message.warning(res.data?.message || "An error occurred. Please try again later.")
107 - }
108 - })
109 - .catch(err => {
110 - if (err.response.status !== 404) {
111 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
112 - }
113 - })
114 - .finally(() => {
115 - loadingLicense.value = false
116 - })
117 -}
118 -
119 -function getLicenseFeatures() {
120 - loadingFeatures.value = true
31 +const key = ref<LicenseKey | undefined>(undefined)
32 +const details = ref<License | undefined>(undefined)
33
122 - Api.license
123 - .getLicenseFeatures()
124 - .then(res => {
125 - if (res.data.success) {
126 - features.value = res.data?.features
127 - } else {
128 - message.warning(res.data?.message || "An error occurred. Please try again later.")
129 - }
130 - })
131 - .catch(err => {
132 - if (err.response.status !== 404) {
133 - message.error(err.response?.data?.message || "An error occurred. Please try again later.")
134 - }
135 - })
136 - .finally(() => {
137 - loadingFeatures.value = false
138 - })
34 +function licenseKeyLoaded(licenseKey: LicenseKey) {
35 + key.value = licenseKey
36 + emit("licenseKeyLoaded", licenseKey)
37 }
38
141 -function load() {
142 - getLicense()
143 - getLicenseFeatures()
39 +function licenseLoaded(license: License) {
40 + details.value = license
41 }
145 -
146 -onBeforeMount(() => {
147 - load()
148 -})
149 -
150 -onMounted(() => {
151 - emit("mounted", {
152 - reload: load
153 - })
154 -})
42 </script>
43
44 <style lang="scss" scoped>
158 -.license-box {
159 - background-color: var(--bg-color);
160 - border-radius: var(--border-radius);
161 - padding: 14px 18px;
162 - .section {
163 - display: flex;
164 - flex-direction: column;
165 - gap: 6px;
166 - .label {
167 - display: flex;
168 - align-items: center;
169 - gap: 10px;
170 - color: var(--fg-secondary-color);
171 - font-family: var(--font-family-mono);
172 - font-size: 14px;
45 +.license-viewer {
46 + height: 100%;
47 +
48 + .main-box {
49 + width: 450px;
50 + min-width: 300px;
51 + }
52 + .side-box {
53 + overflow: hidden;
54 + transition: all 0.3s var(--bezier-ease);
55 + }
56 + &.has-side {
57 + align-items: stretch;
58 + .side-box {
59 + flex-grow: 1;
60 }
61 + }
62 +
63 + @media (max-width: 800px) {
64 + flex-direction: column;
65
175 - .value {
176 - font-size: 16px;
177 - font-weight: bold;
66 + .main-box {
67 + width: unset;
68 + min-width: unset;
69 + max-width: unset;
70 }
71 }
72 }
frontend/src/components/license/SubscriptionCard.vue new
+190
@@ -0,0 +1,190 @@
1 +<template>
2 + <div
3 + class="license-subscription-feature-box"
4 + :class="{ embedded, disabled }"
5 + @click="selectable ? () => {} : (showDetails = true)"
6 + >
7 + <n-spin :show="canceling" content-class="px-4 py-3 flex flex-col gap-2">
8 + <div class="header-box flex justify-between items-center">
9 + <div class="flex items-center gap-2 cursor-pointer">
10 + <span>{{ subscription.name }}</span>
11 + <span class="info-btn pt-0.5" @click.stop="showDetails = true" v-if="selectable">
12 + <Icon :name="InfoIcon" :size="14"></Icon>
13 + </span>
14 + </div>
15 + <div class="price">
16 + {{ price(subscription.price) }}
17 + </div>
18 + </div>
19 + <div class="main-box flex items-center gap-3" v-if="!hideDetails">
20 + <div class="content flex flex-col gap-2 grow">
21 + <div class="title">{{ subscription.info }}</div>
22 + <div class="description">
23 + {{ subscription.short_description }}
24 + </div>
25 + <div class="flex items-center justify-end" v-if="showDeleteOnCard && licenseData">
26 + <n-popconfirm @positive-click="cancelSubscription()">
27 + <template #trigger>
28 + <n-button text size="small" class="opacity-50">
29 + <template #icon>
30 + <Icon :name="DeleteIcon" :size="16"></Icon>
31 + </template>
32 + Unsubscribe
33 + </n-button>
34 + </template>
35 + {{ deleteMessage }}
36 + </n-popconfirm>
37 + </div>
38 + </div>
39 + </div>
40 + </n-spin>
41 +
42 + <n-modal
43 + v-model:show="showDetails"
44 + preset="card"
45 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
46 + :title="subscription.name"
47 + :bordered="false"
48 + content-class="flex flex-col"
49 + segmented
50 + >
51 + <n-spin :show="canceling" content-class="flex flex-col gap-4 grow" class="flex flex-col grow">
52 + <div class="flex gap-4 justify-between">
53 + <div>{{ subscription.info }}</div>
54 + <div class="font-mono whitespace-nowrap text-primary-color">
55 + {{ price(subscription.price) }}
56 + </div>
57 + </div>
58 + <div class="grow">{{ subscription.full_description }}</div>
59 + <div class="flex items-center justify-end" v-if="showDeleteOnDialog && licenseData">
60 + <n-popconfirm to="body" @positive-click="cancelSubscription()">
61 + <template #trigger>
62 + <n-button text size="small" class="opacity-50">
63 + <template #icon>
64 + <Icon :name="DeleteIcon" :size="16"></Icon>
65 + </template>
66 + Unsubscribe
67 + </n-button>
68 + </template>
69 + {{ deleteMessage }}
70 + </n-popconfirm>
71 + </div>
72 + </n-spin>
73 + </n-modal>
74 + </div>
75 +</template>
76 +
77 +<script setup lang="ts">
78 +import Icon from "@/components/common/Icon.vue"
79 +import { ref, toRefs } from "vue"
80 +import { NSpin, NModal, NButton, NPopconfirm, useMessage } from "naive-ui"
81 +import Api from "@/api"
82 +import type { License, SubscriptionFeature } from "@/types/license"
83 +import { price } from "@/utils"
84 +import type { CancelSubscriptionPayload } from "@/api/license"
85 +
86 +const emit = defineEmits<{
87 + (e: "deleted"): void
88 +}>()
89 +
90 +const props = defineProps<{
91 + subscription: SubscriptionFeature
92 + embedded?: boolean
93 + selectable?: boolean
94 + disabled?: boolean
95 + hideDetails?: boolean
96 + showDeleteOnCard?: boolean
97 + showDeleteOnDialog?: boolean
98 + licenseData?: License
99 +}>()
100 +const { subscription, embedded, selectable, disabled, hideDetails, showDeleteOnCard, showDeleteOnDialog, licenseData } =
101 + toRefs(props)
102 +
103 +const DeleteIcon = "ph:minus-circle"
104 +const InfoIcon = "carbon:information"
105 +const message = useMessage()
106 +const showDetails = ref(false)
107 +const deleteMessage = "Are you sure you want to give up this feature?"
108 +const canceling = ref(false)
109 +
110 +function cancelSubscription() {
111 + canceling.value = true
112 +
113 + const payload: CancelSubscriptionPayload = {
114 + customer_email: licenseData.value?.customer.email || "",
115 + subscription_price_id: subscription.value.subscription_price_id,
116 + feature_name: subscription.value.name
117 + }
118 +
119 + Api.license
120 + .cancelSubscription(payload)
121 + .then(res => {
122 + if (res.data.success) {
123 + emit("deleted")
124 + } else {
125 + message.warning(res.data?.message || "An error occurred. Please try again later.")
126 + }
127 + })
128 + .catch(err => {
129 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
130 + })
131 + .finally(() => {
132 + canceling.value = false
133 + })
134 +}
135 +</script>
136 +
137 +<style lang="scss" scoped>
138 +.license-subscription-feature-box {
139 + border-radius: var(--border-radius);
140 + background-color: var(--bg-color);
141 + border: var(--border-small-050);
142 + transition: all 0.2s var(--bezier-ease);
143 + cursor: pointer;
144 +
145 + .header-box {
146 + font-size: 13px;
147 + line-height: 1.25;
148 + .price {
149 + font-size: 15px;
150 + font-family: var(--font-family-mono);
151 + color: var(--primary-color);
152 + }
153 +
154 + .info-btn {
155 + &:hover {
156 + color: var(--primary-color);
157 + }
158 + }
159 + }
160 +
161 + .main-box {
162 + .content {
163 + word-break: break-word;
164 +
165 + .description {
166 + color: var(--fg-secondary-color);
167 + font-size: 13px;
168 + }
169 + }
170 + }
171 +
172 + &.embedded {
173 + background-color: var(--bg-secondary-color);
174 + }
175 +
176 + &.disabled {
177 + cursor: not-allowed;
178 +
179 + & > div {
180 + opacity: 0.5;
181 + }
182 + }
183 +
184 + &:not(.disabled) {
185 + &:hover {
186 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
187 + }
188 + }
189 +}
190 +</style>
frontend/src/components/license/bkp/LicenseCheckout.vue new
+117
@@ -0,0 +1,117 @@
1 +<template>
2 + <div class="license-box flex gap-1 items-center justify-between">
3 + <div class="flex flex-col gap-1">
4 + <p class="flex gap-4 items-center">
5 + <span>license</span>
6 + <Icon :name="LoadingIcon" v-if="loadingLicense"></Icon>
7 + </p>
8 +
9 + <h3 v-if="!loadingLicense">
10 + {{ licenseKey || "No license found" }}
11 + </h3>
12 + </div>
13 +
14 + <div class="actions-box flex gap-2 mr-2" v-if="!loadingLicense">
15 + <n-button type="primary" @click="openCheckout()">
16 + <template #icon>
17 + <Icon :name="licenseKey ? ExtendIcon : LicenseIcon"></Icon>
18 + </template>
19 + {{ licenseKey ? "Extend license" : "Create new license" }}
20 + </n-button>
21 + </div>
22 + </div>
23 +
24 + <n-modal
25 + v-model:show="showCheckoutForm"
26 + preset="card"
27 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
28 + :title="licenseKey ? 'Update License' : 'Add License'"
29 + :bordered="false"
30 + content-class="flex flex-col"
31 + segmented
32 + >
33 + <LicenseCheckoutWizard />
34 + </n-modal>
35 +</template>
36 +
37 +<script setup lang="ts">
38 +import { NButton, NSpin, NModal, useMessage } from "naive-ui"
39 +import Icon from "@/components/common/Icon.vue"
40 +import Api from "@/api"
41 +import { onBeforeMount, ref } from "vue"
42 +import LicenseCheckoutWizard from "./LicenseCheckoutWizard.vue"
43 +import type { LicenseKey } from "@/types/license.d"
44 +
45 +const emit = defineEmits<{
46 + (e: "loaded"): void
47 +}>()
48 +
49 +const LoadingIcon = "eos-icons:loading"
50 +const LicenseIcon = "carbon:license"
51 +const ExtendIcon = "carbon:intent-request-create"
52 +
53 +const message = useMessage()
54 +const showCheckoutForm = ref(false)
55 +const loadingLicense = ref(false)
56 +
57 +const licenseKey = ref<LicenseKey | "">("")
58 +
59 +function openCheckout() {
60 + showCheckoutForm.value = true
61 +}
62 +
63 +function getLicense() {
64 + loadingLicense.value = true
65 +
66 + Api.license
67 + .getLicense()
68 + .then(res => {
69 + if (res.data.success) {
70 + //licenseKey.value = res.data?.license_key || ""
71 + if (licenseKey.value) {
72 + emit("loaded")
73 + }
74 + } else {
75 + message.warning(res.data?.message || "An error occurred. Please try again later.")
76 + }
77 + })
78 + .catch(err => {
79 + if (err.response.status !== 404) {
80 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
81 + }
82 + })
83 + .finally(() => {
84 + loadingLicense.value = false
85 + })
86 +}
87 +
88 +onBeforeMount(() => {
89 + getLicense()
90 +})
91 +</script>
92 +
93 +<style lang="scss" scoped>
94 +.license-box {
95 + background-color: var(--bg-color);
96 + border-radius: var(--border-radius);
97 + padding: 14px 18px;
98 + .section {
99 + display: flex;
100 + flex-direction: column;
101 + gap: 6px;
102 + .label {
103 + display: flex;
104 + align-items: center;
105 + gap: 10px;
106 + color: var(--fg-secondary-color);
107 + font-family: var(--font-family-mono);
108 + font-size: 14px;
109 + }
110 +
111 + .value {
112 + font-size: 16px;
113 + font-weight: bold;
114 + }
115 + }
116 +}
117 +</style>
frontend/src/components/license/bkp/LicenseEditor.vue renamed
frontend/src/components/reportCreation/Wizard.vue
-1
@@ -155,7 +155,6 @@ watch(selectedPanelsIds, () => {
155 watch(
156 [timeValue, timeUnit],
157 ([value, unit]) => {
158 - console.log("timerange")
158 emit("timerange", `${value}${unit}`)
159 },
160 { immediate: true }
frontend/src/components/soc/SocAlerts/SocAlertsList.vue
-1
@@ -296,7 +296,6 @@ function purge() {
296 }
297
298 function itemDeleted(alertId: string, noEmit = false) {
299 - console.log("itemDeleted", alertId, noEmit)
299 clearChecked(alertId)
300 getAlerts()
301 if (!noEmit) {
frontend/src/layouts/HorizontalNav/MainContainer.vue
+17
@@ -56,12 +56,24 @@ onMounted(() => {
56 & > .n-scrollbar-rail {
57 top: calc(var(--toolbar-height) + 2px);
58 }
59 +
60 + & > .n-scrollbar-container {
61 + & > .n-scrollbar-content {
62 + min-height: 100%;
63 + display: flex;
64 + flex-direction: column;
65 + }
66 + }
67 }
68 }
69
70 .view {
71 padding: var(--view-padding);
72 padding-top: 0;
73 + flex-grow: 1;
74 + width: 100%;
75 + display: flex;
76 + flex-direction: column;
77
78 &.boxed {
79 max-width: var(--boxed-width);
@@ -71,6 +83,11 @@ onMounted(() => {
83
84 @media (max-width: $sidebar-bp) {
85 padding-left: 0px;
86 +
87 + .view {
88 + padding-top: calc(var(--view-padding) / 2);
89 + }
90 +
91 &.sidebar-collapsed {
92 padding-left: 0px;
93 }
frontend/src/layouts/VerticalNav/MainContainer.vue
+1
@@ -64,6 +64,7 @@ onMounted(() => {
64 .view {
65 padding: var(--view-padding);
66 padding-top: calc(var(--view-padding) / 2);
67 + width: 100%;
68
69 &.boxed {
70 max-width: var(--boxed-width);
frontend/src/layouts/common/Toolbar/PinnedPages.vue
+9 -9
@@ -41,7 +41,7 @@
41 </template>
42
43 <script lang="ts" setup>
44 -import { useRouter, type RouteRecordName } from "vue-router"
44 +import { useRouter, type RouteLocationNormalized, type RouteRecordName } from "vue-router"
45 import { type RemovableRef, useStorage } from "@vueuse/core"
46 import { computed, type ComputedRef } from "vue"
47 import { NTag } from "naive-ui"
@@ -50,18 +50,13 @@ import _split from "lodash/split"
50 import _uniqBy from "lodash/uniqBy"
51 import Icon from "@/components/common/Icon.vue"
52
53 -const PinnedIcon = "tabler:pinned"
54 -
53 interface Page {
54 name: RouteRecordName | string
55 fullPath: string
56 title: string
57 }
58
61 -defineOptions({
62 - name: "PinnedPages"
63 -})
64 -
59 +const PinnedIcon = "tabler:pinned"
60 const router = useRouter()
61
62 const removeLatestPage = (pageName: RouteRecordName | string) => {
@@ -76,6 +71,7 @@ const gotoPage = (pageName: RouteRecordName | string) => {
71 router.push({ name: pageName })
72 return true
73 }
74 +
75 const pinPage = (page: Page) => {
76 const isPresent = pinned.value.findIndex(p => p.name === page.name) !== -1
77 if (!isPresent) {
@@ -93,10 +89,10 @@ const latestSanitized: ComputedRef<Page[]> = computed(() => {
89 ) as Page[]
90 })
91
96 -router.afterEach(route => {
92 +const checkRoute = (route: RouteLocationNormalized) => {
93 const title = route.meta?.title || _split(route.name?.toString(), "-").at(-1)
94
99 - if (route.name && title) {
95 + if (route.name && title && !route.meta?.skipPin) {
96 const page: Page = {
97 name: route.name,
98 fullPath: route.fullPath,
@@ -104,6 +100,10 @@ router.afterEach(route => {
100 }
101 latest.value = _uniqBy([page, ...latest.value, page], "name")
102 }
103 +}
104 +
105 +router.afterEach(route => {
106 + checkRoute(route)
107 })
108 </script>
109
frontend/src/router-env.d.ts
+1
@@ -9,5 +9,6 @@ declare module "vue-router" {
9 interface RouteMeta extends RouteMetaAuth {
10 title?: string
11 forceLayout?: Layout
12 + skipPin?: boolean
13 }
14 }
frontend/src/router/index.ts
+46 -16
@@ -4,7 +4,7 @@ import Login from "@/views/Auth/Login.vue"
4 import { UserRole } from "@/types/auth.d"
5 import { Layout } from "@/types/theme.d"
6 import { authCheck } from "@/utils/auth"
7 -import type { FormType } from "@/components/AuthForm/index.vue"
7 +import type { FormType } from "@/components/AuthForm/types.d"
8
9 const router = createRouter({
10 history: createWebHistory(import.meta.env.BASE_URL),
@@ -33,15 +33,24 @@ const router = createRouter({
33 },
34 {
35 path: "/agents",
36 - name: "Agents",
37 - component: () => import("@/views/Agents.vue"),
38 - meta: { title: "Agents", auth: true, roles: UserRole.All }
39 - },
40 - {
41 - path: "/agent/:id?",
42 - name: "Agent",
43 - component: () => import("@/views/AgentOverview.vue"),
44 - meta: { title: "Agent", auth: true, roles: UserRole.All }
36 + meta: {
37 + auth: true,
38 + roles: UserRole.All
39 + },
40 + children: [
41 + {
42 + path: "",
43 + name: "Agents",
44 + component: () => import("@/views/agents/Agents.vue"),
45 + meta: { title: "Agents" }
46 + },
47 + {
48 + path: ":id",
49 + name: "Agent",
50 + component: () => import("@/views/agents/Overview.vue"),
51 + meta: { title: "Agent" }
52 + }
53 + ]
54 },
55 {
56 path: "/graylog",
@@ -149,9 +158,30 @@ const router = createRouter({
158 },
159 {
160 path: "/license",
152 - name: "License",
153 - component: () => import("@/views/License.vue"),
154 - meta: { title: "License", auth: true, roles: UserRole.All }
161 + meta: {
162 + auth: true,
163 + roles: UserRole.All
164 + },
165 + children: [
166 + {
167 + path: "",
168 + name: "License",
169 + component: () => import("@/views/license/License.vue"),
170 + meta: { title: "License" }
171 + },
172 + {
173 + path: "success",
174 + name: "LicenseSuccess",
175 + component: () => import("@/views/license/Success.vue"),
176 + meta: { title: "License Success", skipPin: true }
177 + },
178 + {
179 + path: "cancel",
180 + name: "LicenseCancel",
181 + component: () => import("@/views/license/Cancel.vue"),
182 + meta: { title: "License Cancel", skipPin: true }
183 + }
184 + ]
185 },
186
187 {
@@ -164,14 +194,14 @@ const router = createRouter({
194 path: "/login",
195 name: "Login",
196 component: Login,
167 - meta: { title: "Login", forceLayout: Layout.Blank, checkAuth: true }
197 + meta: { title: "Login", forceLayout: Layout.Blank, checkAuth: true, skipPin: true }
198 },
199 {
200 path: "/register",
201 name: "Register",
202 component: () => import("@/views/Auth/Login.vue"),
203 props: { formType: "signup" as FormType },
174 - meta: { title: "Register", forceLayout: Layout.Blank, checkAuth: true }
204 + meta: { title: "Register", forceLayout: Layout.Blank, checkAuth: true, skipPin: true }
205 },
206 {
207 path: "/logout",
@@ -182,7 +212,7 @@ const router = createRouter({
212 path: "/:pathMatch(.*)*",
213 name: "NotFound",
214 component: () => import("@/views/NotFound.vue"),
185 - meta: { forceLayout: Layout.Blank }
215 + meta: { forceLayout: Layout.Blank, skipPin: true }
216 }
217 ]
218 })
frontend/src/types/customers.d.ts
+1
@@ -83,6 +83,7 @@ export interface CustomerProvision {
83 grafana_url: string
84 provision_wazuh_worker: boolean
85 provision_ha_proxy: boolean
86 + dfir_iris_username: string
87 }
88
89 export interface CustomerDecomissionedData {
frontend/src/types/license.d.ts
+144 -12
@@ -27,22 +27,154 @@ export interface License {
27 }
28
29 export interface LicenseCustomer {
30 - Id: number
31 - Name: string
32 - Email: string
33 - CompanyName: string
34 - Created: number
30 + id: number
31 + name: string
32 + email: string
33 + companyName: string
34 + created: number
35 }
36
37 export interface LicenseDataObject {
38 - Id: number
39 - Name: string
40 - StringValue: string
41 - IntValue: number
38 + id: number
39 + name: string
40 + stringValue: string
41 + intValue: number
42 }
43
44 -export enum LicenseFeatures {
45 - "Reporting" = "REPORTING"
46 -}
44 +export type LicenseFeatures = "REPORTING" | "THREAT INTEL" | "HUNTRESS" | "MIMECAST" | "CARBONBLACK"
45
46 export type LicenseKey = `${string}-${string}-${string}-${string}`
47 +
48 +export interface SubscriptionFeature {
49 + id: number
50 + subscription_price_id: string
51 + name: LicenseFeatures
52 + price: number
53 + currency: string
54 + info: string
55 + short_description: string
56 + full_description: string
57 +}
58 +
59 +export interface CheckoutPayload {
60 + feature_id: number
61 + cancel_url: string
62 + success_url: string
63 + customer_email: string
64 + company_name: string
65 +}
66 +
67 +export interface LicenseCheckoutSession {
68 + after_expiration: string | null
69 + allow_promotion_codes: string | null
70 + amount_subtotal: number
71 + amount_total: number
72 + automatic_tax: AutomaticTax
73 + billing_address_collection: string | null
74 + cancel_url: string
75 + client_reference_id: string | null
76 + client_secret: string | null
77 + consent: string | null
78 + consent_collection: string | null
79 + created: number
80 + currency: string
81 + currency_conversion: string | null
82 + custom_fields: { [key: string]: string }
83 + custom_text: CustomText
84 + customer: string | null
85 + customer_creation: string
86 + customer_details: LicenseCheckoutCustomerDetails
87 + customer_email: string
88 + expires_at: number
89 + id: string
90 + invoice: string | null
91 + invoice_creation: InvoiceCreation | null
92 + livemode: boolean
93 + locale: string | null
94 + metadata: Metadata
95 + mode: string
96 + object: string
97 + payment_intent: string | null
98 + payment_link: string | null
99 + payment_method_collection: string
100 + payment_method_configuration_details: string | null
101 + payment_method_options: PaymentMethodOptions
102 + payment_method_types: string[]
103 + payment_status: string
104 + phone_number_collection: PhoneNumberCollection
105 + recovered_from: string | null
106 + setup_intent: string | null
107 + shipping_address_collection: string | null
108 + shipping_cost: string | null
109 + shipping_details: string | null
110 + shipping_options: { [key: string]: string }
111 + status: string
112 + submit_type: string | null
113 + subscription: string | null
114 + success_url: string
115 + total_details: TotalDetails
116 + ui_mode: string
117 + url: string
118 +}
119 +
120 +export interface AutomaticTax {
121 + enabled: boolean
122 + liability: string | null
123 + status: string | null
124 +}
125 +
126 +export interface CustomText {
127 + after_submit: string | null
128 + shipping_address: string | null
129 + submit: string | null
130 + terms_of_service_acceptance: string | null
131 +}
132 +
133 +export interface LicenseCheckoutCustomerDetails {
134 + address: string | null
135 + email: string
136 + name: string | null
137 + phone: string | null
138 + tax_exempt: string
139 + tax_ids: string | null
140 +}
141 +
142 +export interface InvoiceCreation {
143 + enabled: boolean
144 + invoice_data: InvoiceData
145 +}
146 +
147 +export interface InvoiceData {
148 + account_tax_ids: string
149 + custom_fields: string
150 + description: string
151 + footer: string
152 + issuer: string
153 + metadata: { [key: string]: string }
154 + rendering_options: string
155 +}
156 +
157 +export interface PaymentMethodOptions {
158 + card: Card
159 +}
160 +
161 +export interface Card {
162 + request_three_d_secure: string
163 +}
164 +
165 +export interface PhoneNumberCollection {
166 + enabled: boolean
167 +}
168 +
169 +export interface TotalDetails {
170 + amount_discount: number
171 + amount_shipping: number
172 + amount_tax: number
173 +}
174 +
175 +export interface Metadata {
176 + company_name: string
177 + feature_id: string
178 + product_id: string
179 + user_id: string
180 +}
frontend/src/utils/index.ts
+19
@@ -91,3 +91,22 @@ export function formatDate(date: Date | string | number, format: string) {
91
92 return datejs.format(format)
93 }
94 +
95 +export function price(
96 + amount: number,
97 + options: { currency?: "USD" | "EUR"; splitDecimal?: boolean } = { currency: "USD", splitDecimal: true }
98 +) {
99 + let symbol = ""
100 + switch (options.currency) {
101 + case "USD":
102 + symbol = "$"
103 + break
104 + case "EUR":
105 + symbol = "€"
106 + break
107 + }
108 +
109 + const price = options.splitDecimal ? (amount / 100).toFixed(2) : amount
110 +
111 + return `${symbol}${price}`
112 +}
frontend/src/views/License.vue deleted
-20
@@ -1,20 +0,0 @@
1 -<template>
2 - <div class="page flex flex-col gap-8">
3 - <LicenseEditor @updated="reload()" />
4 - <LicenseViewer @mounted="licenseViewerCTX = $event" hide-key />
5 - </div>
6 -</template>
7 -
8 -<script setup lang="ts">
9 -import LicenseEditor from "@/components/license/LicenseEditor.vue"
10 -import LicenseViewer from "@/components/license/LicenseViewer.vue"
11 -import { ref } from "vue"
12 -
13 -const licenseViewerCTX = ref<{ reload: () => void } | null>(null)
14 -
15 -function reload() {
16 - if (licenseViewerCTX.value) {
17 - licenseViewerCTX.value.reload()
18 - }
19 -}
20 -</script>
frontend/src/views/ReportCreation.vue
+9 -17
@@ -8,22 +8,27 @@
8 hide-panels-select
9 />
10 <ReportPanels :timerange="timerange" :org="org" :dashboard="dashboard" :panels="panels" />
11 - <div class="mobile-overlay">
12 - <div>
13 - <Icon :name="AlertIcon" :size="18" class="relative top-0.5 mr-1"></Icon>
11 + <div class="overlay mobile-overlay">
12 + <n-alert>
13 + <template #icon>
14 + <Icon :name="AlertIcon" :size="18"></Icon>
15 + </template>
16 This function is available only for desktop devices
15 - </div>
17 + </n-alert>
18 </div>
19 + <LicenseFeatureOverlay :feature="'REPORTING'" />
20 </div>
21 </template>
22
23 <script setup lang="ts">
24 import { ref } from "vue"
25 +import { NAlert } from "naive-ui"
26 import ReportWizard from "@/components/reportCreation/Wizard.vue"
27 import ReportPanels from "@/components/reportCreation/Panels.vue"
28 import type { Dashboard, Org, Panel } from "@/types/reporting.d"
29 import type { ReportTimeRange } from "@/api/reporting"
30 import Icon from "@/components/common/Icon.vue"
31 +import LicenseFeatureOverlay from "@/components/license/LicenseFeatureOverlay.vue"
32
33 const AlertIcon = "mdi:alert-outline"
34
@@ -41,20 +46,7 @@ const panels = ref<Panel[]>([])
46 position: relative;
47
48 .mobile-overlay {
44 - background-color: rgba(var(--bg-body-rgb), 0.8);
45 - position: absolute;
46 - top: 0;
47 - left: 0;
48 - right: 0;
49 - bottom: 0;
50 - display: flex;
51 - align-items: center;
52 - justify-content: center;
53 - padding: 40px;
54 - text-align: center;
55 - font-size: 20px;
49 display: none;
57 -
50 @media (max-width: $sidebar-bp) {
51 display: flex;
52 }
frontend/src/views/agents/Agents.vue renamed
frontend/src/views/agents/Overview.vue renamed
+1 -1
@@ -222,7 +222,7 @@ onBeforeMount(() => {
222 getAgent()
223 })
224 } else {
225 - router.replace(`/agents`).catch(() => {})
225 + router.replace({ name: "Agents" }).catch(() => {})
226 }
227 })
228 </script>
frontend/src/views/license/Cancel.vue new
+8
@@ -0,0 +1,8 @@
1 +<template>
2 + <div class="page grow flex flex-col justify-center items-center">
3 + <LicenseCheckoutResponse class="max-w-96" type="error" />
4 + </div>
5 +</template>
6 +<script setup lang="ts">
7 +import LicenseCheckoutResponse from "@/components/license/LicenseCheckoutResponse.vue"
8 +</script>
frontend/src/views/license/License.vue new
+28
@@ -0,0 +1,28 @@
1 +<template>
2 + <div class="page grow flex flex-col" :class="{ 'limit-height': licenseKey }">
3 + <LicenseViewer
4 + class="grow"
5 + :class="{ 'overflow-hidden': licenseKey }"
6 + @license-key-loaded="licenseKey = $event"
7 + />
8 + </div>
9 +</template>
10 +
11 +<script setup lang="ts">
12 +import LicenseViewer from "@/components/license/LicenseViewer.vue"
13 +import type { LicenseKey } from "@/types/license"
14 +import { ref } from "vue"
15 +
16 +const licenseKey = ref<LicenseKey | undefined>(undefined)
17 +</script>
18 +
19 +<style lang="scss" scoped>
20 +.page {
21 + &.limit-height {
22 + @media (min-width: 801px) {
23 + max-height: calc(100svh - var(--toolbar-height) - var(--view-padding) - var(--header-bar-height) - 66px);
24 + overflow: hidden;
25 + }
26 + }
27 +}
28 +</style>
frontend/src/views/license/Success.vue new
+11
@@ -0,0 +1,11 @@
1 +<template>
2 + <div class="page grow flex flex-col justify-center items-center">
3 + <LicenseCheckoutResponse class="max-w-96" type="success" :data="{ email: route.query.email?.toString() }" />
4 + </div>
5 +</template>
6 +<script setup lang="ts">
7 +import { useRoute } from "vue-router"
8 +import LicenseCheckoutResponse from "@/components/license/LicenseCheckoutResponse.vue"
9 +
10 +const route = useRoute()
11 +</script>
frontend/ui.excalidraw new
+605
@@ -0,0 +1,605 @@
1 +{
2 + "type": "excalidraw",
3 + "version": 2,
4 + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
5 + "elements": [
6 + {
7 + "type": "rectangle",
8 + "version": 159,
9 + "versionNonce": 524733706,
10 + "isDeleted": false,
11 + "id": "WCXubkJqwY1EBjWdoZ4UF",
12 + "fillStyle": "solid",
13 + "strokeWidth": 2,
14 + "strokeStyle": "solid",
15 + "roughness": 1,
16 + "opacity": 100,
17 + "angle": 0,
18 + "x": 14.140625,
19 + "y": -1095.7784090909095,
20 + "strokeColor": "#1e1e1e",
21 + "backgroundColor": "transparent",
22 + "width": 1035.8203124999998,
23 + "height": 1347.2109374999995,
24 + "seed": 820134410,
25 + "groupIds": [],
26 + "frameId": null,
27 + "roundness": {
28 + "type": 3
29 + },
30 + "boundElements": [],
31 + "updated": 1712331056177,
32 + "link": null,
33 + "locked": false
34 + },
35 + {
36 + "type": "text",
37 + "version": 164,
38 + "versionNonce": 1431519933,
39 + "isDeleted": false,
40 + "id": "f5HHS4R2eos3pcfewyCRm",
41 + "fillStyle": "solid",
42 + "strokeWidth": 2,
43 + "strokeStyle": "solid",
44 + "roughness": 1,
45 + "opacity": 100,
46 + "angle": 0,
47 + "x": 198.7578125,
48 + "y": -1002.1518465909094,
49 + "strokeColor": "#1e1e1e",
50 + "backgroundColor": "transparent",
51 + "width": 569.53125,
52 + "height": 43.199999999999996,
53 + "seed": 203518154,
54 + "groupIds": [],
55 + "frameId": null,
56 + "roundness": null,
57 + "boundElements": [],
58 + "updated": 1712333788867,
59 + "link": null,
60 + "locked": false,
61 + "fontSize": 36,
62 + "fontFamily": 3,
63 + "text": "Add featurs to unlock power",
64 + "textAlign": "left",
65 + "verticalAlign": "top",
66 + "containerId": null,
67 + "originalText": "Add featurs to unlock power",
68 + "lineHeight": 1.2,
69 + "baseline": 35
70 + },
71 + {
72 + "type": "rectangle",
73 + "version": 135,
74 + "versionNonce": 1941740490,
75 + "isDeleted": false,
76 + "id": "kf0f-2Om75MkypwXS174o",
77 + "fillStyle": "hachure",
78 + "strokeWidth": 2,
79 + "strokeStyle": "solid",
80 + "roughness": 1,
81 + "opacity": 100,
82 + "angle": 0,
83 + "x": 117.203125,
84 + "y": -823.0596590909093,
85 + "strokeColor": "#1e1e1e",
86 + "backgroundColor": "#a5d8ff",
87 + "width": 826.4531249999998,
88 + "height": 984.9062499999997,
89 + "seed": 2097989514,
90 + "groupIds": [],
91 + "frameId": null,
92 + "roundness": {
93 + "type": 3
94 + },
95 + "boundElements": [],
96 + "updated": 1712331056177,
97 + "link": null,
98 + "locked": false
99 + },
100 + {
101 + "type": "rectangle",
102 + "version": 295,
103 + "versionNonce": 1032137482,
104 + "isDeleted": false,
105 + "id": "7SJrl4Dw-SUauJXSiJF14",
106 + "fillStyle": "hachure",
107 + "strokeWidth": 2,
108 + "strokeStyle": "solid",
109 + "roughness": 1,
110 + "opacity": 100,
111 + "angle": 0,
112 + "x": -1108.9086326979468,
113 + "y": -1294.9356671554262,
114 + "strokeColor": "#e03131",
115 + "backgroundColor": "transparent",
116 + "width": 3217.2379032258073,
117 + "height": 1870.816532258065,
118 + "seed": 1272923722,
119 + "groupIds": [],
120 + "frameId": null,
121 + "roundness": null,
122 + "boundElements": [],
123 + "updated": 1712331158762,
124 + "link": null,
125 + "locked": false
126 + },
127 + {
128 + "type": "rectangle",
129 + "version": 338,
130 + "versionNonce": 1807733974,
131 + "isDeleted": false,
132 + "id": "Q_YlaJ-8C95ZGoHhRWjC1",
133 + "fillStyle": "solid",
134 + "strokeWidth": 2,
135 + "strokeStyle": "solid",
136 + "roughness": 1,
137 + "opacity": 100,
138 + "angle": 0,
139 + "x": -888.4846793831173,
140 + "y": 1179.2799310064945,
141 + "strokeColor": "#1e1e1e",
142 + "backgroundColor": "transparent",
143 + "width": 932.2488839285713,
144 + "height": 1347.2109374999995,
145 + "seed": 1480733898,
146 + "groupIds": [],
147 + "frameId": null,
148 + "roundness": {
149 + "type": 3
150 + },
151 + "boundElements": [],
152 + "updated": 1712331118544,
153 + "link": null,
154 + "locked": false
155 + },
156 + {
157 + "type": "text",
158 + "version": 284,
159 + "versionNonce": 1317344147,
160 + "isDeleted": false,
161 + "id": "t2JR-U6P-TEZ1kFlzMKlt",
162 + "fillStyle": "solid",
163 + "strokeWidth": 2,
164 + "strokeStyle": "solid",
165 + "roughness": 1,
166 + "opacity": 100,
167 + "angle": 0,
168 + "x": -809.1827820616888,
169 + "y": 1255.244663149352,
170 + "strokeColor": "#1e1e1e",
171 + "backgroundColor": "transparent",
172 + "width": 274.21875,
173 + "height": 43.199999999999996,
174 + "seed": 1170647690,
175 + "groupIds": [],
176 + "frameId": null,
177 + "roundness": null,
178 + "boundElements": [],
179 + "updated": 1712333788867,
180 + "link": null,
181 + "locked": false,
182 + "fontSize": 36,
183 + "fontFamily": 3,
184 + "text": "Your features",
185 + "textAlign": "left",
186 + "verticalAlign": "top",
187 + "containerId": null,
188 + "originalText": "Your features",
189 + "lineHeight": 1.2,
190 + "baseline": 35
191 + },
192 + {
193 + "type": "rectangle",
194 + "version": 412,
195 + "versionNonce": 1144292694,
196 + "isDeleted": false,
197 + "id": "GVKje1B7wQDilUq2QjfT8",
198 + "fillStyle": "hachure",
199 + "strokeWidth": 2,
200 + "strokeStyle": "solid",
201 + "roughness": 1,
202 + "opacity": 100,
203 + "angle": 0,
204 + "x": -795.2715097402604,
205 + "y": 1366.0611810064945,
206 + "strokeColor": "#1e1e1e",
207 + "backgroundColor": "#a5d8ff",
208 + "width": 760.6746651785709,
209 + "height": 220.98325892857127,
210 + "seed": 1521204298,
211 + "groupIds": [],
212 + "frameId": null,
213 + "roundness": {
214 + "type": 3
215 + },
216 + "boundElements": [],
217 + "updated": 1712331057596,
218 + "link": null,
219 + "locked": false
220 + },
221 + {
222 + "type": "rectangle",
223 + "version": 346,
224 + "versionNonce": 1602330582,
225 + "isDeleted": false,
226 + "id": "GjXsh91Zb0JnjA2513ETO",
227 + "fillStyle": "hachure",
228 + "strokeWidth": 2,
229 + "strokeStyle": "solid",
230 + "roughness": 1,
231 + "opacity": 100,
232 + "angle": 0,
233 + "x": -1043.0350531524928,
234 + "y": 913.4095032991206,
235 + "strokeColor": "#e03131",
236 + "backgroundColor": "transparent",
237 + "width": 3217.2379032258073,
238 + "height": 1870.816532258065,
239 + "seed": 1575818058,
240 + "groupIds": [],
241 + "frameId": null,
242 + "roundness": null,
243 + "boundElements": [],
244 + "updated": 1712331160262,
245 + "link": null,
246 + "locked": false
247 + },
248 + {
249 + "type": "rectangle",
250 + "version": 458,
251 + "versionNonce": 1032729622,
252 + "isDeleted": false,
253 + "id": "tUHThzmjGIwgBIaCq8GPQ",
254 + "fillStyle": "hachure",
255 + "strokeWidth": 2,
256 + "strokeStyle": "solid",
257 + "roughness": 1,
258 + "opacity": 100,
259 + "angle": 0,
260 + "x": -791.1141436688317,
261 + "y": 1635.4668729707803,
262 + "strokeColor": "#1e1e1e",
263 + "backgroundColor": "#a5d8ff",
264 + "width": 760.6746651785709,
265 + "height": 220.98325892857127,
266 + "seed": 273495882,
267 + "groupIds": [],
268 + "frameId": null,
269 + "roundness": {
270 + "type": 3
271 + },
272 + "boundElements": [],
273 + "updated": 1712331058945,
274 + "link": null,
275 + "locked": false
276 + },
277 + {
278 + "type": "line",
279 + "version": 49,
280 + "versionNonce": 1737468682,
281 + "isDeleted": false,
282 + "id": "TqLcU40EPWKV32-qNn9Mv",
283 + "fillStyle": "hachure",
284 + "strokeWidth": 2,
285 + "strokeStyle": "solid",
286 + "roughness": 1,
287 + "opacity": 100,
288 + "angle": 0,
289 + "x": -884.2287259111847,
290 + "y": 2304.458512253878,
291 + "strokeColor": "#1e1e1e",
292 + "backgroundColor": "transparent",
293 + "width": 935.5887276785713,
294 + "height": 0,
295 + "seed": 169125526,
296 + "groupIds": [],
297 + "frameId": null,
298 + "roundness": null,
299 + "boundElements": [],
300 + "updated": 1712331071945,
301 + "link": null,
302 + "locked": false,
303 + "startBinding": null,
304 + "endBinding": null,
305 + "lastCommittedPoint": null,
306 + "startArrowhead": null,
307 + "endArrowhead": null,
308 + "points": [
309 + [
310 + 0,
311 + 0
312 + ],
313 + [
314 + 935.5887276785713,
315 + 0
316 + ]
317 + ]
318 + },
319 + {
320 + "type": "rectangle",
321 + "version": 50,
322 + "versionNonce": 617744586,
323 + "isDeleted": false,
324 + "id": "oiA0_cm-MxJTTxEGVA39b",
325 + "fillStyle": "hachure",
326 + "strokeWidth": 2,
327 + "strokeStyle": "solid",
328 + "roughness": 1,
329 + "opacity": 100,
330 + "angle": 0,
331 + "x": -824.8537259111847,
332 + "y": 2354.012083682449,
333 + "strokeColor": "#1e1e1e",
334 + "backgroundColor": "#ffec99",
335 + "width": 805.9709821428571,
336 + "height": 132.17075892857156,
337 + "seed": 1839772950,
338 + "groupIds": [],
339 + "frameId": null,
340 + "roundness": {
341 + "type": 3
342 + },
343 + "boundElements": [
344 + {
345 + "type": "text",
346 + "id": "Q0FSEEZIspJb-fxFIRNvZ"
347 + }
348 + ],
349 + "updated": 1712331080935,
350 + "link": null,
351 + "locked": false
352 + },
353 + {
354 + "type": "text",
355 + "version": 13,
356 + "versionNonce": 845381597,
357 + "isDeleted": false,
358 + "id": "Q0FSEEZIspJb-fxFIRNvZ",
359 + "fillStyle": "hachure",
360 + "strokeWidth": 2,
361 + "strokeStyle": "solid",
362 + "roughness": 1,
363 + "opacity": 100,
364 + "angle": 0,
365 + "x": -537.8838598397562,
366 + "y": 2398.497463146735,
367 + "strokeColor": "#1e1e1e",
368 + "backgroundColor": "#ffec99",
369 + "width": 232.03125,
370 + "height": 43.199999999999996,
371 + "seed": 1369711510,
372 + "groupIds": [],
373 + "frameId": null,
374 + "roundness": null,
375 + "boundElements": [],
376 + "updated": 1712333793473,
377 + "link": null,
378 + "locked": false,
379 + "fontSize": 36,
380 + "fontFamily": 3,
381 + "text": "add feature",
382 + "textAlign": "center",
383 + "verticalAlign": "middle",
384 + "containerId": "oiA0_cm-MxJTTxEGVA39b",
385 + "originalText": "add feature",
386 + "lineHeight": 1.2,
387 + "baseline": 35
388 + },
389 + {
390 + "type": "rectangle",
391 + "version": 449,
392 + "versionNonce": 2045072918,
393 + "isDeleted": false,
394 + "id": "dpsEDP9fMx3RQ_Zw20GLK",
395 + "fillStyle": "solid",
396 + "strokeWidth": 2,
397 + "strokeStyle": "solid",
398 + "roughness": 1,
399 + "opacity": 100,
400 + "angle": 0,
401 + "x": 95.3183340097396,
402 + "y": 1174.3831676136374,
403 + "strokeColor": "#1e1e1e",
404 + "backgroundColor": "transparent",
405 + "width": 1866.0100446428573,
406 + "height": 1347.2109374999995,
407 + "seed": 487683734,
408 + "groupIds": [],
409 + "frameId": null,
410 + "roundness": {
411 + "type": 3
412 + },
413 + "boundElements": [],
414 + "updated": 1712331123430,
415 + "link": null,
416 + "locked": false
417 + },
418 + {
419 + "type": "text",
420 + "version": 350,
421 + "versionNonce": 381018909,
422 + "isDeleted": false,
423 + "id": "WZpG35_7Xp53DBnXfgerm",
424 + "fillStyle": "solid",
425 + "strokeWidth": 2,
426 + "strokeStyle": "solid",
427 + "roughness": 1,
428 + "opacity": 100,
429 + "angle": 0,
430 + "x": 176.50360186688226,
431 + "y": 1263.2245738636377,
432 + "strokeColor": "#1e1e1e",
433 + "backgroundColor": "transparent",
434 + "width": 147.65625,
435 + "height": 43.199999999999996,
436 + "seed": 1338996886,
437 + "groupIds": [],
438 + "frameId": null,
439 + "roundness": null,
440 + "boundElements": [],
441 + "updated": 1712333788867,
442 + "link": null,
443 + "locked": false,
444 + "fontSize": 36,
445 + "fontFamily": 3,
446 + "text": "Details",
447 + "textAlign": "left",
448 + "verticalAlign": "top",
449 + "containerId": null,
450 + "originalText": "Details",
451 + "lineHeight": 1.2,
452 + "baseline": 35
453 + },
454 + {
455 + "type": "rectangle",
456 + "version": 64,
457 + "versionNonce": 328294538,
458 + "isDeleted": false,
459 + "id": "-ElCTpzQ3xaCiYT-X4dXl",
460 + "fillStyle": "hachure",
461 + "strokeWidth": 2,
462 + "strokeStyle": "solid",
463 + "roughness": 1,
464 + "opacity": 100,
465 + "angle": 0,
466 + "x": 191.05029194595772,
467 + "y": 1365.228601539592,
468 + "strokeColor": "#1e1e1e",
469 + "backgroundColor": "#b2f2bb",
470 + "width": 1557.5474330357142,
471 + "height": 143.47098214285734,
472 + "seed": 806531030,
473 + "groupIds": [],
474 + "frameId": null,
475 + "roundness": {
476 + "type": 3
477 + },
478 + "boundElements": [],
479 + "updated": 1712331146599,
480 + "link": null,
481 + "locked": false
482 + },
483 + {
484 + "type": "rectangle",
485 + "version": 105,
486 + "versionNonce": 1761052374,
487 + "isDeleted": false,
488 + "id": "LWqStHnyaoG8w0S2URNGv",
489 + "fillStyle": "hachure",
490 + "strokeWidth": 2,
491 + "strokeStyle": "solid",
492 + "roughness": 1,
493 + "opacity": 100,
494 + "angle": 0,
495 + "x": 189.25062676738617,
496 + "y": 1576.6404318967348,
497 + "strokeColor": "#1e1e1e",
498 + "backgroundColor": "#b2f2bb",
499 + "width": 1557.5474330357142,
500 + "height": 143.47098214285734,
501 + "seed": 1586258634,
502 + "groupIds": [],
503 + "frameId": null,
504 + "roundness": {
505 + "type": 3
506 + },
507 + "boundElements": [],
508 + "updated": 1712331147514,
509 + "link": null,
510 + "locked": false
511 + },
512 + {
513 + "type": "rectangle",
514 + "version": 153,
515 + "versionNonce": 590816010,
516 + "isDeleted": false,
517 + "id": "WycKd7HWykIHf6xFdSAM_",
518 + "fillStyle": "hachure",
519 + "strokeWidth": 2,
520 + "strokeStyle": "solid",
521 + "roughness": 1,
522 + "opacity": 100,
523 + "angle": 0,
524 + "x": 195.41692141024305,
525 + "y": 1777.8820613610205,
526 + "strokeColor": "#1e1e1e",
527 + "backgroundColor": "#b2f2bb",
528 + "width": 1557.5474330357142,
529 + "height": 143.47098214285734,
530 + "seed": 44500310,
531 + "groupIds": [],
532 + "frameId": null,
533 + "roundness": {
534 + "type": 3
535 + },
536 + "boundElements": [],
537 + "updated": 1712331148401,
538 + "link": null,
539 + "locked": false
540 + },
541 + {
542 + "type": "rectangle",
543 + "version": 198,
544 + "versionNonce": 613269846,
545 + "isDeleted": false,
546 + "id": "yuhS5wfbhCXtiHqd5BDgx",
547 + "fillStyle": "hachure",
548 + "strokeWidth": 2,
549 + "strokeStyle": "solid",
550 + "roughness": 1,
551 + "opacity": 100,
552 + "angle": 0,
553 + "x": 197.07707766024305,
554 + "y": 1980.4490256467348,
555 + "strokeColor": "#1e1e1e",
556 + "backgroundColor": "#b2f2bb",
557 + "width": 1557.5474330357142,
558 + "height": 143.47098214285734,
559 + "seed": 1213513802,
560 + "groupIds": [],
561 + "frameId": null,
562 + "roundness": {
563 + "type": 3
564 + },
565 + "boundElements": [],
566 + "updated": 1712331150000,
567 + "link": null,
568 + "locked": false
569 + },
570 + {
571 + "type": "rectangle",
572 + "version": 243,
573 + "versionNonce": 711506646,
574 + "isDeleted": false,
575 + "id": "64UJCaHuygytFtzxrKkQR",
576 + "fillStyle": "hachure",
577 + "strokeWidth": 2,
578 + "strokeStyle": "solid",
579 + "roughness": 1,
580 + "opacity": 100,
581 + "angle": 0,
582 + "x": 203.3689303388146,
583 + "y": 2174.617552432449,
584 + "strokeColor": "#1e1e1e",
585 + "backgroundColor": "#b2f2bb",
586 + "width": 1557.5474330357142,
587 + "height": 143.47098214285734,
588 + "seed": 2071900298,
589 + "groupIds": [],
590 + "frameId": null,
591 + "roundness": {
592 + "type": 3
593 + },
594 + "boundElements": [],
595 + "updated": 1712331151011,
596 + "link": null,
597 + "locked": false
598 + }
599 + ],
600 + "appState": {
601 + "gridSize": null,
602 + "viewBackgroundColor": "#ffffff"
603 + },
604 + "files": {}
605 +}
src/components/connectors/ConfigForm/FormTypes/FileType.vue new
+97
@@ -0,0 +1,97 @@
1 +<template>
2 + <el-form :model="form" status-icon :rules="rules" label-width="120px" ref="formRef" label-position="top">
3 + <el-form-item label="File" prop="connector_file">
4 + <el-upload
5 + class="file-upload-wrap"
6 + drag
7 + :limit="1"
8 + :auto-upload="false"
9 + :on-exceed="handleExceed"
10 + :on-change="handleChange"
11 + ref="uploadRef"
12 + accept=".yaml, .YAML"
13 + >
14 + <el-icon class="el-icon--upload"><upload-filled /></el-icon>
15 + <div class="el-upload__text">
16 + Drop file here or
17 + <em>click to upload</em>
18 + </div>
19 + <template #tip>
20 + <div class="el-upload__tip text-red">Limit 1 file .YAML, new file will cover the old file</div>
21 + </template>
22 + </el-upload>
23 + </el-form-item>
24 + </el-form>
25 +</template>
26 +
27 +<script setup lang="ts">
28 +import { FormInstance } from "element-plus"
29 +import type { UploadFile, UploadInstance, UploadProps, UploadRawFile } from "element-plus"
30 +import { UploadFilled } from "@element-plus/icons-vue"
31 +import { onMounted, reactive, ref, toRefs } from "vue"
32 +
33 +export interface IFileForm {
34 + connector_file: File | null
35 +}
36 +
37 +const emit = defineEmits<{
38 + (e: "mounted", value: FormInstance): void
39 +}>()
40 +
41 +const props = defineProps<{
42 + form: IFileForm
43 +}>()
44 +const { form } = toRefs(props)
45 +
46 +const formRef = ref<FormInstance>()
47 +const uploadRef = ref<UploadInstance>()
48 +
49 +const handleExceed: UploadProps["onExceed"] = (files: File[]) => {
50 + uploadRef.value!.clearFiles()
51 + const file = files[0] as UploadRawFile
52 + uploadRef.value!.handleStart(file)
53 +}
54 +
55 +const handleChange: UploadProps["onChange"] = (file: UploadFile) => {
56 + form.value.connector_file = file.raw
57 +}
58 +
59 +const validateFile = (rule: any, value: File, callback: any) => {
60 + console.log(value)
61 + if (!value) {
62 + return callback(new Error("Please input a valid File"))
63 + }
64 +
65 + if (value.type.indexOf("yaml") === -1) {
66 + return callback(new Error("Please input a valid File"))
67 + }
68 +
69 + return callback()
70 +}
71 +
72 +const rules = reactive({
73 + connector_file: [{ required: true, validator: validateFile, trigger: "blur" }]
74 +})
75 +
76 +onMounted(() => {
77 + if (formRef.value) {
78 + emit("mounted", formRef.value)
79 + }
80 +})
81 +</script>
82 +
83 +<style scoped lang="scss">
84 +.file-upload-wrap {
85 + width: 100%;
86 +
87 + .el-icon--upload {
88 + margin-bottom: 0px;
89 + }
90 +
91 + :deep() {
92 + .el-upload-dragger {
93 + padding: 20px 10px;
94 + }
95 + }
96 +}
97 +</style>