@cryptotaxi247 / CoPilot / commits / 16f47cb1

Scheduler page (#201)

* add job description to scheduler * Update example for dashboards in ProvisionNewCustomer class * updated dependencies * updated logs page link location * Add logging statement to execute_integration function in integrations.py * put office365 route * added scheduler api/types * updated login components * added scheduler page * updated scheduler api * tmp * tmp * updated job type * updated job card * Update monitoring_alert.py and monitoring_alert.schema with response model changes * Refactor monitoring_alert.py and monitoring_alert.schema with response model changes * Refactor monitoring_alert.py and monitoring_alert.schema with response model changes * updated scheduler page icon * refactor props * Add delete_monitoring_alert endpoint to monitoring_alert.py * Update branch name in Docker workflow from 'scheduler-page' to 'main' * added job actions component * Refactor provision_content_pack function to accept ProvisionContentPackRequest in graylog/routes/provision.py Added fortinet content pack templates * Update docker-compose.yml to version v0.0.8 * add network connectors db things * added network connectors population to db * network connectors routes * Update fortinet.md * Update fortinet.md * Update fortinet.md * Update fortinet.md * Update fortinet.md * Create opnsense.md * Update opnsense.md * Update opnsense.md * Fix error handling in provision.py * Fix error handling in provision.py * Add route to remove a user from a customer in dfir_iris/routes/users.py * Fix error handling in provision.py * Fix error handling in provision.py * Refactor customer network connector processing in routes.py * fortinet provisioning initial setup * just about all fortinet provision stuff...but need to still assign newly created stream to the pipeline * updated scheduler api * added next run time component * updated dependencies * updated job actions component * added job form * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com> Co-authored-by: juan-socfortress <111928961+juan-socfortress@users.noreply.github.com>

taylor_socfortress committed Apr 26, 2024 at 16:02 UTC 16f47cb1ef017c5f448e29a47ef9e82810e0d86e
72 files changed +4085 -323
README.md
+2 -3
@@ -84,15 +84,14 @@ systemctl restart docker
84
85 ```bash
86 # Clone the CoPilot repository
87 -wget https://raw.githubusercontent.com/socfortress/CoPilot/v0.0.7/docker-compose.yml
87 +wget https://raw.githubusercontent.com/socfortress/CoPilot/v0.0.8/docker-compose.yml
88
89 # Edit the docker-compose.yml file to set the server name and/or the services you want to use
90
91 # Create the path for storing your data
92 mkdir data
93
94 -# Copy .env.example to .env
95 -cp .env.example .env
94 +# Create the .env file based on the .env.example
95
96 # Run Copilot
97 docker compose up -d
backend/alembic/alembic.ini
+1 -1
@@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
60 # are written from script.py.mako
61 # output_encoding = utf-8
62
63 -sqlalchemy.url = mysql+pymysql://copilot:REPLACE_WITH_PASS@copilot-mysql/copilot
63 +sqlalchemy.url = mysql+pymysql://copilot:REPLACE_ME@copilot-mysql/copilot
64
65
66 [post_write_hooks]
backend/alembic/env.py
+13
@@ -26,6 +26,19 @@ from app.integrations.alert_creation_settings.models.alert_creation_settings imp
26 )
27 from app.integrations.models.customer_integration_settings import CustomerIntegrations
28 from app.integrations.monitoring_alert.models.monitoring_alert import MonitoringAlerts
29 +from app.network_connectors.models.network_connectors import AvailableNetworkConnectors
30 +from app.network_connectors.models.network_connectors import (
31 + AvailableNetworkConnectorsKeys,
32 +)
33 +from app.network_connectors.models.network_connectors import CustomerNetworkConnectors
34 +from app.network_connectors.models.network_connectors import (
35 + CustomerNetworkConnectorsMeta,
36 +)
37 +from app.network_connectors.models.network_connectors import NetworkConnectorsConfig
38 +from app.network_connectors.models.network_connectors import NetworkConnectorsService
39 +from app.network_connectors.models.network_connectors import (
40 + NetworkConnectorsSubscription,
41 +)
42 from app.schedulers.models.scheduler import JobMetadata
43
44 # this is the Alembic Config object, which provides
backend/alembic/versions/74a095d63af4_add_network_connectors_tables.py new
+123
@@ -0,0 +1,123 @@
1 +"""Add Network Connectors Tables
2 +
3 +Revision ID: 74a095d63af4
4 +Revises: c3ad5012f4db
5 +Create Date: 2024-04-25 13:03:22.718120
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +from sqlalchemy.dialects import mysql
13 +
14 +from alembic import op
15 +
16 +# revision identifiers, used by Alembic.
17 +revision: str = "74a095d63af4"
18 +down_revision: Union[str, None] = "c3ad5012f4db"
19 +branch_labels: Union[str, Sequence[str], None] = None
20 +depends_on: Union[str, Sequence[str], None] = None
21 +
22 +
23 +def upgrade() -> None:
24 + # ### commands auto generated by Alembic - please adjust! ###
25 + op.create_table(
26 + "available_network_connectors",
27 + sa.Column("id", sa.Integer(), nullable=False),
28 + sa.Column("network_connector_name", sa.String(length=255), nullable=False),
29 + sa.Column("description", sa.String(length=1024), nullable=False),
30 + sa.Column("network_connector_details", mysql.TEXT(length=1000000), nullable=False),
31 + sa.PrimaryKeyConstraint("id"),
32 + )
33 + op.create_table(
34 + "customer_network_connectors",
35 + sa.Column("id", sa.Integer(), nullable=False),
36 + sa.Column("customer_code", sa.String(length=50), nullable=False),
37 + sa.Column("customer_name", sa.String(length=255), nullable=False),
38 + sa.Column("network_connector_service_id", sa.Integer(), nullable=False),
39 + sa.Column("network_connector_service_name", sa.String(length=255), nullable=False),
40 + sa.Column("deployed", sa.Boolean(), nullable=False),
41 + sa.PrimaryKeyConstraint("id"),
42 + )
43 + op.create_table(
44 + "customer_network_connectors_meta",
45 + sa.Column("id", sa.Integer(), nullable=False),
46 + sa.Column("customer_code", sa.String(length=50), nullable=False),
47 + sa.Column("network_connector_name", sa.String(length=255), nullable=False),
48 + sa.Column("graylog_input_id", sa.String(length=1024), nullable=True),
49 + sa.Column("graylog_index_id", sa.String(length=1024), nullable=False),
50 + sa.Column("graylog_stream_id", sa.String(length=1024), nullable=False),
51 + sa.Column("grafana_org_id", sa.String(length=1024), nullable=False),
52 + sa.Column("grafana_dashboard_folder_id", sa.String(length=1024), nullable=False),
53 + sa.PrimaryKeyConstraint("id"),
54 + )
55 + op.create_table(
56 + "network_connectors_services",
57 + sa.Column("id", sa.Integer(), nullable=False),
58 + sa.Column("service_name", sa.String(length=255), nullable=False),
59 + sa.Column("auth_type", sa.String(length=50), nullable=False),
60 + sa.PrimaryKeyConstraint("id"),
61 + )
62 + op.create_table(
63 + "available_network_connectors_keys",
64 + sa.Column("id", sa.Integer(), nullable=False),
65 + sa.Column("network_connector_id", sa.Integer(), nullable=True),
66 + sa.Column("network_connector_name", sa.String(length=255), nullable=False),
67 + sa.Column("auth_key_name", sa.String(length=255), nullable=False),
68 + sa.ForeignKeyConstraint(
69 + ["network_connector_id"],
70 + ["available_network_connectors.id"],
71 + ),
72 + sa.PrimaryKeyConstraint("id"),
73 + )
74 + op.create_table(
75 + "network_connectors_configs",
76 + sa.Column("id", sa.Integer(), nullable=False),
77 + sa.Column("network_connector_service_id", sa.Integer(), nullable=True),
78 + sa.Column("config_key", sa.String(length=255), nullable=False),
79 + sa.Column("config_value", sa.String(length=1024), nullable=False),
80 + sa.ForeignKeyConstraint(
81 + ["network_connector_service_id"],
82 + ["network_connectors_services.id"],
83 + ),
84 + sa.PrimaryKeyConstraint("id"),
85 + )
86 + op.create_table(
87 + "network_connectors_subscriptions",
88 + sa.Column("id", sa.Integer(), nullable=False),
89 + sa.Column("customer_id", sa.Integer(), nullable=True),
90 + sa.Column("network_connectors_service_id", sa.Integer(), nullable=False),
91 + sa.ForeignKeyConstraint(
92 + ["customer_id"],
93 + ["customer_network_connectors.id"],
94 + ),
95 + sa.ForeignKeyConstraint(
96 + ["network_connectors_service_id"],
97 + ["network_connectors_services.id"],
98 + ),
99 + sa.PrimaryKeyConstraint("id"),
100 + )
101 + op.create_table(
102 + "network_connectors_keys",
103 + sa.Column("id", sa.Integer(), nullable=True),
104 + sa.Column("subscription_id", sa.Integer(), nullable=True),
105 + sa.Column("auth_key_name", sa.String(length=255), nullable=False),
106 + sa.Column("auth_value", sa.String(length=1024), nullable=False),
107 + sa.ForeignKeyConstraint(["subscription_id"], ["network_connectors_subscriptions.id"]),
108 + sa.PrimaryKeyConstraint("id"),
109 + )
110 + # ### end Alembic commands ###
111 +
112 +
113 +def downgrade() -> None:
114 + # ### commands auto generated by Alembic - please adjust! ###
115 + op.drop_table("network_connectors_keys")
116 + op.drop_table("network_connectors_subscriptions")
117 + op.drop_table("network_connectors_configs")
118 + op.drop_table("available_network_connectors_keys")
119 + op.drop_table("network_connectors_services")
120 + op.drop_table("customer_network_connectors_meta")
121 + op.drop_table("customer_network_connectors")
122 + op.drop_table("available_network_connectors")
123 + # ### end Alembic commands ###
backend/alembic/versions/c3ad5012f4db_add_job_description_to_job_metadata_.py new
+31
@@ -0,0 +1,31 @@
1 +"""Add Job Description to Job Metadata Table
2 +
3 +Revision ID: c3ad5012f4db
4 +Revises: bdf40d064ed1
5 +Create Date: 2024-04-23 14:11:01.313978
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +
13 +from alembic import op
14 +
15 +# revision identifiers, used by Alembic.
16 +revision: str = "c3ad5012f4db"
17 +down_revision: Union[str, None] = "bdf40d064ed1"
18 +branch_labels: Union[str, Sequence[str], None] = None
19 +depends_on: Union[str, Sequence[str], None] = None
20 +
21 +
22 +def upgrade() -> None:
23 + # ### commands auto generated by Alembic - please adjust! ###
24 + op.add_column("scheduled_job_metadata", sa.Column("job_description", sa.String(length=1024), nullable=True))
25 + # ### end Alembic commands ###
26 +
27 +
28 +def downgrade() -> None:
29 + # ### commands auto generated by Alembic - please adjust! ###
30 + pass
31 + # ### end Alembic commands ###
backend/app/connectors/dfir_iris/routes/users.py
+39
@@ -8,6 +8,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 UserRemovedFromCustomerResponse
12 from app.connectors.dfir_iris.schema.users import UsersResponse
13 from app.connectors.dfir_iris.services.users import assign_user_to_alert
14 from app.connectors.dfir_iris.services.users import delete_user_from_alert
@@ -134,6 +135,44 @@ async def add_user_to_customers_route(
135 raise HTTPException(status_code=400, detail=f"Failed to add user {user_id} to customers {customers}")
136
137
138 +@dfir_iris_users_router.delete(
139 + "/remove/{user_id}/{customer_id}",
140 + response_model=AlertResponse,
141 + description="Remove a user from a customer",
142 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
143 +)
144 +async def remove_user_from_customer_route(
145 + user_id: int,
146 + customer_id: str,
147 +) -> UserRemovedFromCustomerResponse:
148 + """
149 + Remove a user from a customer.
150 +
151 + Parameters:
152 + - customer_id (str): The ID of the customer.
153 + - user_id (int): The ID of the user.
154 +
155 + Returns:
156 + - AlertResponse: The response containing the removed user.
157 +
158 + Raises:
159 + - HTTPException: If the customer or user does not exist.
160 + """
161 + customers = await collect_all_customers()
162 + customer_ids = [str(customer["customer_id"]) for customer in customers]
163 + if customer_id in customer_ids:
164 + customer_ids.remove(customer_id)
165 + else:
166 + raise HTTPException(status_code=404, detail="Customer ID not found")
167 + logger.info(f"Customer IDs: {customer_ids}")
168 + logger.info(f"Removing user {user_id} from customers {customer_ids}")
169 + success = await add_user_to_customers(customer_ids, user_id)
170 + if success:
171 + return UserRemovedFromCustomerResponse(message=f"User {user_id} removed from customer {customer_id}", success=True)
172 + else:
173 + raise HTTPException(status_code=400, detail=f"Failed to remove user {user_id} from customer {customer_id}")
174 +
175 +
176 @dfir_iris_users_router.delete(
177 "/assign/{alert_id}/{user_id}",
178 response_model=AlertResponse,
backend/app/connectors/dfir_iris/schema/users.py
+5
@@ -20,3 +20,8 @@ class UsersResponse(BaseModel):
20 class UserAddedToCustomerResponse(BaseModel):
21 success: bool
22 message: str
23 +
24 +
25 +class UserRemovedFromCustomerResponse(BaseModel):
26 + success: bool
27 + message: str
backend/app/connectors/shuffle/services/integrations.py
+1
@@ -16,4 +16,5 @@ 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"Response: {response}")
20 return response
backend/app/connectors/shuffle/utils/universal.py
+1
@@ -150,6 +150,7 @@ async def send_post_request(
150 json=data,
151 verify=False,
152 )
153 + logger.info(f"Response from Shuffle API: {response.json()}")
154
155 if response.status_code == 204:
156 return {
backend/app/customer_provisioning/routes/provision.py
+42
@@ -18,6 +18,8 @@ from app.customer_provisioning.schema.provision import GetSubscriptionsResponse
18 from app.customer_provisioning.schema.provision import ProvisionDashboardRequest
19 from app.customer_provisioning.schema.provision import ProvisionDashboardResponse
20 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
21 +from app.customer_provisioning.schema.provision import UpdateOffice365OrgIdRequest
22 +from app.customer_provisioning.schema.provision import UpdateOffice365OrgIdResponse
23 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerRequest
24 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerResponse
25 from app.customer_provisioning.services.provision import provision_dashboards
@@ -364,3 +366,43 @@ async def provision_dashboards_route(
366 grafana_url=request.grafana_url,
367 ),
368 )
369 +
370 +
371 +@customer_provisioning_router.put(
372 + "/update/office365_org_id/{customer_code}",
373 + response_model=UpdateOffice365OrgIdResponse,
374 + description="Update Office 365 organization ID",
375 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
376 +)
377 +async def update_office_365_org_id(
378 + customer_code: str,
379 + request: UpdateOffice365OrgIdRequest = Body(...),
380 + session: AsyncSession = Depends(get_db),
381 +):
382 + """
383 + Update Office 365 organization ID for a customer.
384 +
385 + Args:
386 + customer_code (str): The code of the customer to update.
387 + request (UpdateOffice365OrgIdRequest): The request data for updating Office 365 organization ID.
388 + session (AsyncSession): The database session.
389 +
390 + Returns:
391 + UpdateOffice365OrgIdResponse: The response data for the updated Office 365 organization ID.
392 + """
393 + logger.info("Updating Office 365 organization ID")
394 + # Update within the `CustomersMeta` table based on the customer code
395 + stmt = select(CustomersMeta).where(CustomersMeta.customer_code == customer_code)
396 + result = await session.execute(stmt)
397 + customer_meta = result.scalars().first()
398 + if not customer_meta:
399 + raise HTTPException(
400 + status_code=404,
401 + detail=f"Customer meta not found for customer: {customer_code}. Please provision the customer first.",
402 + )
403 + customer_meta.customer_meta_office365_organization_id = request.office365_org_id
404 + await session.commit()
405 + return UpdateOffice365OrgIdResponse(
406 + message="Office 365 organization ID updated successfully",
407 + success=True,
408 + )
backend/app/customer_provisioning/schema/provision.py
+16 -1
@@ -54,7 +54,7 @@ class ProvisionNewCustomer(BaseModel):
54 )
55 customer_subscription: List[CustomerSubsctipion] = Field(
56 ...,
57 - example=["Wazuh", "Office365"],
57 + example=["Wazuh"],
58 description="List of subscriptions for the customer",
59 )
60 dashboards_to_include: DashboardProvisionRequest = Field(
@@ -269,3 +269,18 @@ class ProvisionDashboardResponse(BaseModel):
269 description="Message indicating the status of the request",
270 )
271 success: bool = Field(..., description="Whether the request was successful or not")
272 +
273 +
274 +class UpdateOffice365OrgIdRequest(BaseModel):
275 + office365_org_id: str = Field(
276 + ...,
277 + description="Office 365 organization ID",
278 + )
279 +
280 +
281 +class UpdateOffice365OrgIdResponse(BaseModel):
282 + message: str = Field(
283 + ...,
284 + description="Message indicating the status of the request",
285 + )
286 + success: bool = Field(..., description="Whether the request was successful or not")
backend/app/customer_provisioning/services/decommission.py
+3 -3
@@ -40,6 +40,9 @@ async def decomission_wazuh_customer(
40 """
41 logger.info(f"Decomissioning customer {customer_meta.customer_name}")
42
43 + # Delete DFIR-IRIS Customer
44 + await delete_customer(customer_id=customer_meta.customer_meta_iris_customer_id)
45 +
46 # Delete the Wazuh Agents
47 agents = await gather_wazuh_agents(customer_meta.customer_code)
48 agents_deleted = await delete_wazuh_agents(agents)
@@ -61,9 +64,6 @@ async def decomission_wazuh_customer(
64 organization_id=int(customer_meta.customer_meta_grafana_org_id),
65 )
66
64 - # Delete DFIR-IRIS Customer
65 - await delete_customer(customer_id=customer_meta.customer_meta_iris_customer_id)
66 -
67 # Decommission Wazuh Worker
68 await decommission_wazuh_worker(
69 request=DecommissionWorkerRequest(customer_name=customer_meta.customer_name),
backend/app/customer_provisioning/services/dfir_iris.py
+8 -1
@@ -82,6 +82,13 @@ async def delete_customer(customer_id: int):
82 None
83 """
84 client, admin = await initialize_client_and_admin("DFIR-IRIS")
85 - result = await fetch_and_validate_data(client, admin.delete_customer, customer_id)
85 + try:
86 + result = await fetch_and_validate_data(client, admin.delete_customer, customer_id)
87 + except Exception as e:
88 + logger.error(f"Failed to delete customer: please remove the user from the iris customer within DFIR-IRIS {e}")
89 + raise HTTPException(
90 + status_code=400,
91 + detail="Failed to delete IRIS customer: please remove the user from the iris customer within DFIR-IRIS",
92 + )
93 logger.info(f"Result: {result}")
94 return None
backend/app/db/db_populate.py
+231
@@ -12,6 +12,10 @@ from app.integrations.models.customer_integration_settings import AvailableInteg
12 from app.integrations.models.customer_integration_settings import (
13 AvailableIntegrationsAuthKeys,
14 )
15 +from app.network_connectors.models.network_connectors import AvailableNetworkConnectors
16 +from app.network_connectors.models.network_connectors import (
17 + AvailableNetworkConnectorsKeys,
18 +)
19
20 load_dotenv()
21
@@ -204,6 +208,7 @@ async def add_roles_if_not_exist(session: AsyncSession) -> None:
208 logger.info("Role check and addition completed.")
209
210
211 +# ! AVAILABLE THIRD PARTY INTEGRATIONS ! #
212 def load_available_integrations_data(
213 integration_name: str,
214 description: str,
@@ -449,3 +454,229 @@ async def add_available_integrations_auth_keys_if_not_exist(session: AsyncSessio
454 logger.error(f"Error adding available integration auth keys: {e}")
455 raise e
456 await session.commit()
457 +
458 +
459 +# ! AVAILABLE NETWORK CONNECTORS ! #
460 +def load_available_network_connectors_data(
461 + network_connector_name: str,
462 + description: str,
463 + network_connector_details: str,
464 +):
465 + """
466 + Load available network_connectors data from environment variables.
467 +
468 + Args:
469 + network_connector_name (str): The name of the network_connector.
470 + description (str): The description of the network_connector.
471 +
472 + Returns:
473 + dict: A dictionary containing the network_connector data.
474 + """
475 + logger.info(f"Loading available network_connectors data for {network_connector_name}.")
476 + return {
477 + "network_connector_name": network_connector_name,
478 + "description": description,
479 + "network_connector_details": network_connector_details,
480 + }
481 +
482 +
483 +def load_markdown_for_network_connector(network_connector_name: str) -> str:
484 + """
485 + Load markdown content for a given network_connector from a file.
486 +
487 + Args:
488 + network_connector_name (str): The name of the network_connector.
489 +
490 + Returns:
491 + str: The content of the markdown file.
492 + """
493 + # file_path = os.path.join("network_connectors_markdown", f"{network_connector_name.lower()}.md")
494 + # if space in the network_connector name, replace it with underscore
495 + if " " in network_connector_name:
496 + network_connector_name = network_connector_name.replace(" ", "_")
497 + file_path = os.path.join(
498 + "app",
499 + "network_connectors",
500 + "markdown",
501 + f"{network_connector_name.lower()}.md",
502 + )
503 + try:
504 + with open(file_path, "r") as file:
505 + return file.read()
506 + except FileNotFoundError:
507 + return "No deployment intrusctions available."
508 +
509 +
510 +def get_available_network_connectors_list():
511 + """
512 + Get a list of available network_connectors.
513 +
514 + Returns:
515 + list: A list of available network_connectors data, where each item contains the network_connector name, description, and markdown details.
516 + """
517 + available_network_connectors = [
518 + ("Fortinet", "Integrate Fortinet with SOCFortress."),
519 + # ... Add more available network_connectors as needed ...
520 + ]
521 +
522 + return [
523 + load_available_network_connectors_data(
524 + network_connector_name,
525 + description,
526 + load_markdown_for_network_connector(network_connector_name),
527 + )
528 + for network_connector_name, description in available_network_connectors
529 + ]
530 +
531 +
532 +async def add_available_network_connectors_if_not_exist(session: AsyncSession):
533 + """
534 + Adds available network_connectors to the database if they do not already exist.
535 +
536 + Args:
537 + session (AsyncSession): The database session.
538 +
539 + Returns:
540 + None
541 + """
542 + available_network_connectors_list = get_available_network_connectors_list()
543 +
544 + for available_network_connector_data in available_network_connectors_list:
545 + try:
546 + query = select(AvailableNetworkConnectors).where(
547 + AvailableNetworkConnectors.network_connector_name == available_network_connector_data["network_connector_name"],
548 + )
549 + result = await session.execute(query)
550 + existing_available_network_connector = result.scalars().first()
551 +
552 + if existing_available_network_connector is None:
553 + new_available_network_connector = AvailableNetworkConnectors(
554 + **available_network_connector_data,
555 + )
556 + logger.info(f"New available network_connector: {available_network_connector_data}")
557 + session.add(new_available_network_connector)
558 + logger.info(
559 + f"Added new available network_connector: {available_network_connector_data['network_connector_name']}",
560 + )
561 + except Exception as e:
562 + logger.error(f"Error adding available network_connector: {e}")
563 + await session.rollback()
564 + raise e
565 + await session.commit()
566 + # Close the session
567 + await session.close()
568 +
569 +
570 +def load_available_network_connectors_auth_keys(
571 + network_connector_id: int,
572 + network_connector_name: str,
573 + auth_key_name: str,
574 +):
575 + """
576 + Load available network_connectors auth keys from environment variables.
577 +
578 + Args:
579 + network_connector_id (int): The ID of the network_connector.
580 + network_connector_name (str): The name of the network_connector.
581 + auth_key_name (str): The name of the auth key.
582 +
583 + Returns:
584 + dict: A dictionary containing the auth key data.
585 + """
586 + logger.info(
587 + f"Loading available network_connectors auth keys data for {network_connector_name}.",
588 + )
589 + return {
590 + "network_connector_id": network_connector_id,
591 + "network_connector_name": network_connector_name,
592 + "auth_key_name": auth_key_name,
593 + }
594 +
595 +
596 +async def get_available_network_connectors_auth_keys_list(session: AsyncSession):
597 + """
598 + Get a list of available network_connectors auth keys with their corresponding network_connector IDs.
599 +
600 + Args:
601 + session (AsyncSession): The database session.
602 +
603 + Returns:
604 + list: A list of available network_connectors auth keys data, where each item contains the network_connector ID, network_connector name, and auth key name.
605 + """
606 + available_network_connectors_auth_keys = []
607 + available_network_connectors = [
608 + ("Fortinet", "SYSLOG_PORT"),
609 + # ... Add more available network_connectors auth keys as needed ...
610 + ]
611 + logger.info("Getting available network_connectors auth keys.")
612 + try:
613 + for network_connector_name, auth_key_name in available_network_connectors:
614 + query = select(AvailableNetworkConnectors.id).where(
615 + AvailableNetworkConnectors.network_connector_name == network_connector_name,
616 + )
617 + result = await session.execute(query)
618 + network_connector_id = result.scalars().first()
619 + logger.info(f"Network Connector ID for {network_connector_name}: {network_connector_id}")
620 + if network_connector_id:
621 + logger.info(f"Found network_connector ID for {network_connector_name}: {network_connector_id}")
622 + available_network_connectors_auth_keys.append(
623 + load_available_network_connectors_auth_keys(
624 + network_connector_id,
625 + network_connector_name,
626 + auth_key_name,
627 + ),
628 + )
629 +
630 + return available_network_connectors_auth_keys
631 + except Exception as e:
632 + logger.error(f"Error getting available network_connectors auth keys: {e}")
633 + await session.rollback()
634 + raise e
635 +
636 +
637 +async def add_available_network_connectors_auth_keys_if_not_exist(session: AsyncSession):
638 + """
639 + Adds available network_connectors auth keys to the database if they do not already exist.
640 +
641 + Args:
642 + session (AsyncSession): The database session.
643 +
644 + Returns:
645 + None
646 + """
647 + logger.info("Checking for existence of available network_connectors auth keys.")
648 + available_network_connectors_auth_keys_list = await get_available_network_connectors_auth_keys_list(session=session)
649 + logger.info("Adding available network_connectors auth keys to the database.")
650 + for available_network_connector_auth_keys_data in available_network_connectors_auth_keys_list:
651 + try:
652 + query = select(AvailableNetworkConnectors).where(
653 + AvailableNetworkConnectors.network_connector_name == available_network_connector_auth_keys_data["network_connector_name"],
654 + )
655 + result = await session.execute(query)
656 + existing_network_connector = result.scalars().first()
657 +
658 + if existing_network_connector:
659 + available_network_connector_auth_keys_data["network_connector_id"] = existing_network_connector.id
660 + auth_key_query = select(AvailableNetworkConnectorsKeys).where(
661 + and_(
662 + AvailableNetworkConnectorsKeys.network_connector_id == existing_network_connector.id,
663 + AvailableNetworkConnectorsKeys.auth_key_name == available_network_connector_auth_keys_data["auth_key_name"],
664 + ),
665 + )
666 + auth_key_result = await session.execute(auth_key_query)
667 + existing_auth_key = auth_key_result.scalars().first()
668 +
669 + if existing_auth_key is None:
670 + new_auth_key = AvailableNetworkConnectorsKeys(
671 + **available_network_connector_auth_keys_data,
672 + )
673 + session.add(new_auth_key)
674 + logger.info(
675 + f"Added new available network_connector auth keys: "
676 + f"{available_network_connector_auth_keys_data['auth_key_name']} for "
677 + f"{available_network_connector_auth_keys_data['network_connector_name']}",
678 + )
679 + except Exception as e:
680 + logger.error(f"Error adding available network_connector auth keys: {e}")
681 + raise e
682 + await session.commit()
backend/app/db/db_setup.py
+27
@@ -17,6 +17,8 @@ from app.auth.services.universal import create_scheduler_user
17 from app.auth.services.universal import remove_scheduler_user
18 from app.db.db_populate import add_available_integrations_auth_keys_if_not_exist
19 from app.db.db_populate import add_available_integrations_if_not_exist
20 +from app.db.db_populate import add_available_network_connectors_auth_keys_if_not_exist
21 +from app.db.db_populate import add_available_network_connectors_if_not_exist
22 from app.db.db_populate import add_connectors_if_not_exist
23 from app.db.db_populate import add_roles_if_not_exist
24 from app.db.db_session import SQLALCHEMY_DATABASE_URI
@@ -217,6 +219,31 @@ async def create_available_integrations(async_engine):
219 await session.commit() # Explicit commit if all operations are successful
220
221
222 +async def create_available_network_connectors(async_engine):
223 + """
224 + Creates available network connectors in the database.
225 +
226 + Args:
227 + async_engine (AsyncEngine): The async engine used to connect to the database.
228 +
229 + Returns:
230 + None
231 + """
232 + logger.info("Creating available network connectors")
233 + async with AsyncSession(
234 + async_engine,
235 + ) as session: # Create an AsyncSession, not just a connection
236 + try:
237 + await add_available_network_connectors_if_not_exist(session)
238 + await add_available_network_connectors_auth_keys_if_not_exist(session)
239 + except Exception as e:
240 + logger.error(f"Error creating available integrations: {e}")
241 + await session.rollback() # Explicit rollback on error
242 + raise # Re-raise the exception to handle it further up the call stack
243 + else:
244 + await session.commit() # Explicit commit if all operations are successful
245 +
246 +
247 async def ensure_admin_user(async_engine):
248 """
249 Ensures that an admin user exists in the database.
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+110 -8
@@ -1,4 +1,3 @@
1 -from typing import List
1 from typing import Optional
2
3 from fastapi import APIRouter
@@ -21,7 +20,7 @@ from app.integrations.monitoring_alert.schema.monitoring_alert import (
20 GraylogPostResponse,
21 )
22 from app.integrations.monitoring_alert.schema.monitoring_alert import (
24 - MonitoringAlertsRequestModel,
23 + MonitoringAlertsResponseModel,
24 )
25 from app.integrations.monitoring_alert.schema.monitoring_alert import (
26 MonitoringWazuhAlertsRequestModel,
@@ -44,6 +43,13 @@ from app.integrations.sap_siem.services.sap_siem_suspicious_logins import (
43
44 monitoring_alerts_router = APIRouter()
45
46 +ALERT_ANALYZERS = {
47 + "WAZUH": analyze_wazuh_alerts,
48 + "SURICATA": analyze_suricata_alerts,
49 + "OFFICE365_THREAT_INTEL": analyze_office365_threatintel_alerts,
50 + "OFFICE365_EXCHANGE_ONLINE": analyze_office365_exchange_online_alerts,
51 +}
52 +
53
54 async def get_customer_meta(customer_code: str, session: AsyncSession) -> CustomersMeta:
55 """
@@ -78,12 +84,12 @@ async def get_customer_meta(customer_code: str, session: AsyncSession) -> Custom
84
85 @monitoring_alerts_router.get(
86 "/list",
81 - response_model=List[MonitoringAlertsRequestModel],
87 + response_model=MonitoringAlertsResponseModel,
88 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
89 )
90 async def list_monitoring_alerts(
91 session: AsyncSession = Depends(get_db),
86 -) -> List[MonitoringAlertsRequestModel]:
92 +) -> MonitoringAlertsResponseModel:
93 """
94 List all monitoring alerts.
95
@@ -98,7 +104,88 @@ async def list_monitoring_alerts(
104 monitoring_alerts = await session.execute(select(MonitoringAlerts))
105 monitoring_alerts = monitoring_alerts.scalars().all()
106
101 - return monitoring_alerts
107 + return MonitoringAlertsResponseModel(
108 + monitoring_alerts=monitoring_alerts,
109 + success=True,
110 + message="Monitoring alerts retrieved successfully",
111 + )
112 +
113 +
114 +@monitoring_alerts_router.post(
115 + "/invoke/{monitoring_alert_id}",
116 + response_model=AlertAnalysisResponse,
117 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
118 +)
119 +async def invoke_monitoring_alert(
120 + monitoring_alert_id: int,
121 + session: AsyncSession = Depends(get_db),
122 +) -> AlertAnalysisResponse:
123 + """
124 + Invoke a monitoring alert.
125 +
126 + Args:
127 + monitoring_alert_id (int): The ID of the monitoring alert to invoke.
128 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
129 +
130 + Returns:
131 + AlertAnalysisResponse: The monitoring alert that was invoked.
132 + """
133 + logger.info(f"Invoking monitoring alert: {monitoring_alert_id}")
134 + monitoring_alert = await session.execute(select(MonitoringAlerts).where(MonitoringAlerts.id == monitoring_alert_id))
135 + monitoring_alert = monitoring_alert.scalars().first()
136 + logger.info(f"Found monitoring alert: {monitoring_alert}")
137 +
138 + if not monitoring_alert:
139 + raise HTTPException(status_code=404, detail="Monitoring alert not found")
140 +
141 + customer_meta = await get_customer_meta(monitoring_alert.customer_code, session)
142 +
143 + analyze_alert = ALERT_ANALYZERS.get(monitoring_alert.alert_source)
144 + logger.info(f"Found alert analyzer: {analyze_alert}")
145 +
146 + if analyze_alert:
147 + return await analyze_alert([monitoring_alert], customer_meta, session)
148 + else:
149 + logger.warning(f"Unknown alert source: {monitoring_alert.alert_source}")
150 +
151 + raise HTTPException(status_code=500, detail="Unknown alert source")
152 +
153 +
154 +@monitoring_alerts_router.delete(
155 + "/{monitoring_alert_id}",
156 + response_model=MonitoringAlertsResponseModel,
157 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
158 +)
159 +async def delete_monitoring_alert(
160 + monitoring_alert_id: int,
161 + session: AsyncSession = Depends(get_db),
162 +) -> MonitoringAlertsResponseModel:
163 + """
164 + Delete a monitoring alert.
165 +
166 + Args:
167 + monitoring_alert_id (int): The ID of the monitoring alert to delete.
168 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
169 +
170 + Returns:
171 + MonitoringAlertsResponseModel: The monitoring alert that was deleted.
172 + """
173 + logger.info(f"Deleting monitoring alert: {monitoring_alert_id}")
174 + monitoring_alert = await session.execute(select(MonitoringAlerts).where(MonitoringAlerts.id == monitoring_alert_id))
175 + monitoring_alert = monitoring_alert.scalars().first()
176 + logger.info(f"Found monitoring alert: {monitoring_alert}")
177 +
178 + if not monitoring_alert:
179 + raise HTTPException(status_code=404, detail="Monitoring alert not found")
180 +
181 + await session.delete(monitoring_alert)
182 + await session.commit()
183 +
184 + return MonitoringAlertsResponseModel(
185 + monitoring_alerts=[monitoring_alert],
186 + success=True,
187 + message="Monitoring alert deleted successfully",
188 + )
189
190
191 @monitoring_alerts_router.post("/create", response_model=GraylogPostResponse)
@@ -133,7 +220,13 @@ async def create_monitoring_alert(
220 CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields["CUSTOMER_CODE"],
221 ),
222 )
136 - customer_meta = customer_meta.scalars().first()
223 + try:
224 + customer_meta = customer_meta.scalars().first()
225 + except Exception as e:
226 + logger.error(
227 + f"Error {e} getting customer meta for the customer_meta_office365_organization_id: {monitoring_alert.event.fields['CUSTOMER_CODE']}",
228 + )
229 + raise HTTPException(status_code=500, detail="Error getting customer meta")
230
231 if not customer_meta:
232 raise HTTPException(status_code=404, detail="Customer not found")
@@ -186,7 +279,10 @@ async def create_custom_monitoring_alert(
279 CustomersMeta.customer_code == monitoring_alert.event.fields[field],
280 ),
281 )
189 - customer_meta = customer_meta.scalars().first()
282 + try:
283 + customer_meta = customer_meta.scalars().first()
284 + except Exception as e:
285 + logger.error(f"Error {e} getting customer meta for the customer_code: {monitoring_alert.event.fields[field]}")
286
287 if not customer_meta:
288 logger.info(f"Getting customer meta for customer_meta_office365_organization_id: {monitoring_alert.event.fields[field]}")
@@ -195,7 +291,13 @@ async def create_custom_monitoring_alert(
291 CustomersMeta.customer_meta_office365_organization_id == monitoring_alert.event.fields[field],
292 ),
293 )
198 - customer_meta = customer_meta.scalars().first()
294 + try:
295 + customer_meta = customer_meta.scalars().first()
296 + except Exception as e:
297 + logger.error(
298 + f"Error {e} getting customer meta for the customer_meta_office365_organization_id: {monitoring_alert.event.fields[field]}",
299 + )
300 + raise HTTPException(status_code=500, detail="Error getting customer meta")
301
302 if not customer_meta:
303 raise HTTPException(status_code=404, detail="Customer not found")
backend/app/integrations/monitoring_alert/schema/monitoring_alert.py
+6
@@ -25,6 +25,12 @@ class MonitoringAlertsRequestModel(BaseModel):
25 orm_mode = True
26
27
28 +class MonitoringAlertsResponseModel(BaseModel):
29 + success: bool
30 + message: str
31 + monitoring_alerts: List[MonitoringAlertsRequestModel]
32 +
33 +
34 class MonitoringWazuhAlertsRequestModel(BaseModel):
35 customer_code: str
36
backend/app/integrations/monitoring_alert/services/provision.py
+1 -1
@@ -47,7 +47,7 @@ from app.integrations.monitoring_alert.schema.provision import (
47 from app.integrations.monitoring_alert.schema.provision import (
48 ProvisionWazuhMonitoringAlertResponse,
49 )
50 -from app.stack_provisioning.graylog.routes.provision import get_graylog_version
50 +from app.stack_provisioning.graylog.services.utils import get_graylog_version
51
52 load_dotenv()
53 import uuid
backend/app/integrations/monitoring_alert/services/wazuh.py
+12 -1
@@ -162,7 +162,16 @@ async def fetch_wazuh_indexer_details(alert_id: str, index: str) -> WazuhAlertMo
162 )
163
164 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
165 - response = es_client.get(index=index, id=alert_id)
165 + logger.info(f"Fetching alert from wazuh-indexer: {alert_id}")
166 + try:
167 + response = es_client.get(index=index, id=alert_id)
168 + except Exception as e:
169 + logger.info(f"Error fetching alert from wazuh-indexer: {e}")
170 + raise HTTPException(
171 + status_code=404,
172 + detail=f"Alert not found in Wazuh-Indexer index: {index} with ID: {alert_id}",
173 + )
174 + logger.info(f"Alert retrieved from wazuh-indexer: {response}")
175
176 return WazuhAlertModel(**response)
177
@@ -515,8 +524,10 @@ async def analyze_wazuh_alerts(
524 WazuhAnalysisResponse: The analysis response.
525 """
526 logger.info(f"Analyzing Wazuh alerts with customer_meta: {customer_meta}")
527 + logger.info(f"Analyzing Wazuh alerts: {monitoring_alerts}")
528 alert_detail_service = await AlertDetailsService.create()
529 for alert in monitoring_alerts:
530 + logger.info(f"Analyzing Wazuh alert: {alert.alert_id}")
531 alert_details = await fetch_alert_details(alert)
532 await check_event_exclusion(alert_details, alert_detail_service, session)
533 iris_alert_id = await check_if_open_alert_exists_in_iris(alert_details, session=session)
backend/app/integrations/office365/services/provision.py
+22 -4
@@ -46,6 +46,7 @@ from app.customer_provisioning.services.grafana import create_grafana_folder
46 from app.customer_provisioning.services.grafana import get_opensearch_version
47 from app.customers.routes.customers import get_customer
48 from app.customers.routes.customers import get_customer_meta
49 +from app.db.universal_models import CustomersMeta
50 from app.integrations.models.customer_integration_settings import CustomerIntegrations
51 from app.integrations.office365.schema.provision import PipelineRuleTitles
52 from app.integrations.office365.schema.provision import PipelineTitles
@@ -225,10 +226,10 @@ async def add_api_auth_to_office365_block(customer_code: str, provision_office36
226
227 except Exception as e:
228 logger.error(f"An error occurred: {e}")
228 - # Print the full traceback
229 - import traceback
230 -
231 - traceback.print_exc()
229 + raise HTTPException(
230 + status_code=500,
231 + detail="Error found in ossec.conf. Multiple <ossec_config> blocks found. Remove all additional <ossec_config> blocks and try again.",
232 + )
233
234
235 async def update_wazuh_configuration(
@@ -918,6 +919,7 @@ async def provision_office365(
919 )
920
921 await update_customer_integration_table(customer_code, session)
922 + await update_customermeta_table(customer_code, session, provision_office365_auth_keys.TENANT_ID)
923
924 return ProvisionOffice365Response(
925 success=True,
@@ -951,3 +953,19 @@ async def update_customer_integration_table(
953 await session.commit()
954
955 return None
956 +
957 +
958 +async def update_customermeta_table(customer_code: str, session: AsyncSession, tenant_id: str) -> None:
959 + """
960 + Updates the `customer_meta` table to set the `office365_tenant_id` column to the given tenant_id.
961 +
962 + Args:
963 + customer_code (str): The customer code.
964 + session (AsyncSession): The async session object for making HTTP requests.
965 + """
966 + await session.execute(
967 + update(CustomersMeta).where(CustomersMeta.customer_code == customer_code).values(customer_meta_office365_organization_id=tenant_id),
968 + )
969 + await session.commit()
970 +
971 + return None
backend/app/network_connectors/markdown/fortinet.md new
+53
@@ -0,0 +1,53 @@
1 +# [Fortinet Syslog Forwarding](https://help.fortinet.com/fa/faz50hlp/56/5-6-1/FMG-FAZ/2400_System_Settings/1600_Log%20Forwarding/0400_Configuring.htm)
2 +
3 +This process involves configuring the Fortinet firewall to send logs to an external syslog server.
4 +
5 +### Step 1: Accessing the Firewall
6 +
7 +Log in to your Fortinet firewall using the web interface or FortiGate GUI:
8 +
9 +- Open a web browser.
10 +- Navigate to the IP address of the FortiGate unit (e.g., `https://192.168.1.99`).
11 +- Enter your administrative credentials to log in.
12 +
13 +### Step 2: Configuring the Syslog Server
14 +
15 +Once logged in, follow these steps to configure the syslog server:
16 +
17 +- **Go to Log & Report**
18 + - Navigate to Log & Report on the left-hand sidebar.
19 + - Select Log Settings
20 + - Click on Log Settings to open the logging configuration options.
21 +- **Add a Syslog Server**
22 + - Find the section labeled Syslog Servers and click on Create New.
23 +- **Configure Syslog Server Details**
24 + - **Name:** Enter a recognizable name for your syslog server.
25 + - **IP/Domain:** Enter the IP address or domain name of your syslog server.
26 + - **Reliable:** Select whether to use TCP (reliable) or UDP (faster but less reliable) for log transmission.
27 + - **Port:** Specify the port number on which the syslog server is listening (default is 514).
28 + - **Facility:** Choose the syslog facility to be used (e.g., Local7).
29 + - **Source IP:** (Optional) Specify the source IP address if you want the logs to come from a specific interface IP.
30 +
31 +* _Note_: The syslog format needs to be configured using FortiGate's CLI. Ensure that the format is set to rfc5424
32 +
33 +- **Configure Filters (if needed)**
34 + - You can specify what kind of logs you want to send (e.g., traffic, event, virus, etc.). Select the appropriate log types.
35 +- **Test Connection (if available)**
36 + - Some FortiGate models allow you to test the connection to ensure the syslog server is reachable.
37 +
38 +### Step 3: Saving the Configuration
39 +
40 +After entering all the necessary configurations:
41 +
42 +- Click on OK or Apply to save the settings.
43 +- The FortiGate firewall will now start forwarding logs to the specified syslog server based on your configurations.
44 +
45 +### Step 4: Verify Log Reception
46 +
47 +Check your syslog server to verify that it is receiving logs from the Fortinet firewall. You might need to configure filters or settings on the syslog server side to properly categorize and display incoming logs.
48 +
49 +### Additional Considerations
50 +
51 +- **Security:** Ensure that the network path between your Fortinet firewall and the syslog server is secure. Consider using VPNs or IPsec tunnels if the logs contain sensitive information.
52 +- **Firewall Rules:** Ensure there are no firewall rules blocking the outgoing traffic on the port used for syslog.
53 +- **Backup Configurations:** Always keep a backup of your firewall configurations before making significant changes.
backend/app/network_connectors/markdown/opnsense.md new
+33
@@ -0,0 +1,33 @@
1 +# [OPNSense Syslog Forwarding](https://docs.opnsense.org/manual/settingsmenu.html#logging)
2 +
3 +This process involves configuring the OPNSense firewall to send logs to an external syslog server.
4 +To configure a remote syslog server in OPNsense, follow these steps:
5 +
6 +### Access OPNsense Web Interface:
7 +
8 +- Open a web browser and navigate to the web interface of your OPNsense firewall.
9 +- Enter your administrative credentials to log in.
10 +
11 +### Step 2: Configuring the Syslog Server
12 +
13 +Navigate to System Logs:
14 +
15 +- In the OPNsense web interface, go to _System_ > _Settings_ > _Logging_.
16 +
17 +Configure Remote Syslog Server:
18 +
19 +- Check the box next to _Enable Remote Logging_ to enable remote logging.
20 +- Enter the IP address or hostname of your remote syslog server in the _Remote log servers_ field.
21 +- Optionally, specify the port number (default is 514) and protocol (UDP or TCP) for remote logging.
22 +- Click _Save_ to apply the changes.
23 +
24 +### Verify Configuration:
25 +
26 +- Once the configuration is saved, OPNsense will start sending syslog messages to the specified remote syslog server.
27 +- You can verify that syslog messages are being received on the remote syslog server by checking its logs or monitoring tools.
28 +
29 +### Additional Considerations
30 +
31 +- **Security:** Ensure that the network path between your OPNSense firewall and the syslog server is secure. Consider using VPNs or IPsec tunnels if the logs contain sensitive information.
32 +- **Firewall Rules:** Ensure there are no firewall rules blocking the outgoing traffic on the port used for syslog.
33 +- **Backup Configurations:** Always keep a backup of your firewall configurations before making significant changes.
backend/app/network_connectors/models/network_connectors.py new
+117
@@ -0,0 +1,117 @@
1 +from typing import List
2 +from typing import Optional
3 +
4 +from sqlalchemy import Text
5 +from sqlmodel import Field
6 +from sqlmodel import Relationship
7 +from sqlmodel import SQLModel
8 +
9 +
10 +class AvailableNetworkConnectors(SQLModel, table=True):
11 + __tablename__ = "available_network_connectors"
12 + id: Optional[int] = Field(default=None, primary_key=True)
13 + network_connector_name: str = Field(max_length=255, nullable=False)
14 + description: str = Field(max_length=1024)
15 + network_connector_details: str = Field(sa_column=Text)
16 + # Relationships
17 + network_connector_keys: List["AvailableNetworkConnectorsKeys"] = Relationship(
18 + back_populates="network_connector",
19 + )
20 +
21 +
22 +class AvailableNetworkConnectorsKeys(SQLModel, table=True):
23 + __tablename__ = "available_network_connectors_keys"
24 + id: Optional[int] = Field(default=None, primary_key=True)
25 + network_connector_id: int = Field(default=None, foreign_key="available_network_connectors.id")
26 + network_connector_name: str = Field(max_length=255, nullable=False)
27 + auth_key_name: str = Field(max_length=255, nullable=False)
28 + # Relationships
29 + network_connector: "AvailableNetworkConnectors" = Relationship(back_populates="network_connector_keys")
30 +
31 +
32 +class CustomerNetworkConnectors(SQLModel, table=True):
33 + __tablename__ = "customer_network_connectors"
34 + id: Optional[int] = Field(default=None, primary_key=True)
35 + customer_code: str = Field(max_length=50, nullable=False)
36 + customer_name: str = Field(max_length=255, nullable=False)
37 + network_connector_service_id: Optional[int] = Field(default=None, nullable=False)
38 + network_connector_service_name: str = Field(max_length=255, nullable=False)
39 + deployed: bool = Field(default=False)
40 + # Relationships
41 + network_connectors_subscriptions: List["NetworkConnectorsSubscription"] = Relationship(
42 + back_populates="customer_network_connectors",
43 + )
44 +
45 +
46 +class NetworkConnectorsService(SQLModel, table=True):
47 + __tablename__ = "network_connectors_services"
48 + id: Optional[int] = Field(default=None, primary_key=True)
49 + service_name: str = Field(max_length=255, nullable=False)
50 + auth_type: str = Field(max_length=50) # e.g., OAuth, API Key, etc.
51 + # Relationships
52 + network_connectors_subscriptions: List["NetworkConnectorsSubscription"] = Relationship(
53 + back_populates="network_connectors_service",
54 + )
55 + configs: List["NetworkConnectorsConfig"] = Relationship(
56 + back_populates="network_connectors_service",
57 + )
58 +
59 +
60 +class NetworkConnectorsSubscription(SQLModel, table=True):
61 + __tablename__ = "network_connectors_subscriptions"
62 + id: Optional[int] = Field(default=None, primary_key=True)
63 + customer_id: int = Field(default=None, foreign_key="customer_network_connectors.id")
64 + network_connectors_service_id: int = Field(
65 + default=None,
66 + foreign_key="network_connectors_services.id",
67 + )
68 + # Relationships
69 + customer_network_connectors: "CustomerNetworkConnectors" = Relationship(
70 + back_populates="network_connectors_subscriptions",
71 + )
72 + network_connectors_service: "NetworkConnectorsService" = Relationship(
73 + back_populates="network_connectors_subscriptions",
74 + )
75 + network_connectors_keys: List["NetworkConnectorsKeys"] = Relationship(
76 + back_populates="network_connectors_subscription",
77 + ) # Moved here
78 +
79 +
80 +class NetworkConnectorsConfig(SQLModel, table=True):
81 + __tablename__ = "network_connectors_configs"
82 + id: Optional[int] = Field(default=None, primary_key=True)
83 + network_connector_service_id: int = Field(
84 + default=None,
85 + foreign_key="network_connectors_services.id",
86 + )
87 + config_key: str = Field(max_length=255) # e.g., 'endpoint', 'port', etc.
88 + config_value: str = Field(max_length=1024) # e.g., 'https://api.service.com/v1'
89 + # Relationships
90 + network_connectors_service: "NetworkConnectorsService" = Relationship(back_populates="configs")
91 +
92 +
93 +class NetworkConnectorsKeys(SQLModel, table=True):
94 + __tablename__ = "network_connectors_keys"
95 + id: Optional[int] = Field(default=None, primary_key=True)
96 + subscription_id: int = Field(
97 + default=None,
98 + foreign_key="network_connectors_subscriptions.id",
99 + )
100 + auth_key_name: str = Field(max_length=255) # e.g., 'credentials', 'rate_limit'
101 + auth_value: str = Field(max_length=1024) # e.g., JSON/encrypted credentials
102 + # Relationships
103 + network_connectors_subscription: "NetworkConnectorsSubscription" = Relationship(
104 + back_populates="network_connectors_keys",
105 + ) # Adjusted relationship
106 +
107 +
108 +class CustomerNetworkConnectorsMeta(SQLModel, table=True):
109 + __tablename__ = "customer_network_connectors_meta"
110 + id: Optional[int] = Field(default=None, primary_key=True)
111 + customer_code: str = Field(max_length=50, nullable=False)
112 + network_connector_name: str = Field(max_length=255, nullable=False)
113 + graylog_input_id: Optional[str] = Field(max_length=1024)
114 + graylog_index_id: str = Field(max_length=1024, nullable=False)
115 + graylog_stream_id: str = Field(max_length=1024, nullable=False)
116 + grafana_org_id: str = Field(max_length=1024, nullable=False)
117 + grafana_dashboard_folder_id: str = Field(max_length=1024, nullable=False)
backend/app/network_connectors/routes.py new
+942
@@ -0,0 +1,942 @@
1 +from typing import List
2 +from typing import Optional
3 +
4 +from fastapi import APIRouter
5 +from fastapi import Depends
6 +from fastapi import HTTPException
7 +from fastapi import Security
8 +from loguru import logger
9 +from sqlalchemy import delete
10 +from sqlalchemy import update
11 +from sqlalchemy.exc import NoResultFound
12 +from sqlalchemy.ext.asyncio import AsyncSession
13 +from sqlalchemy.future import select
14 +from sqlalchemy.orm import joinedload
15 +
16 +from app.auth.utils import AuthHandler
17 +from app.db.db_session import get_db
18 +from app.db.universal_models import Customers
19 +from app.db.universal_models import CustomersMeta
20 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
21 + AlertCreationSettings,
22 +)
23 +from app.network_connectors.models.network_connectors import AvailableNetworkConnectors
24 +from app.network_connectors.models.network_connectors import CustomerNetworkConnectors
25 +from app.network_connectors.models.network_connectors import (
26 + CustomerNetworkConnectorsMeta,
27 +)
28 +from app.network_connectors.models.network_connectors import NetworkConnectorsConfig
29 +from app.network_connectors.models.network_connectors import NetworkConnectorsKeys
30 +from app.network_connectors.models.network_connectors import NetworkConnectorsService
31 +from app.network_connectors.models.network_connectors import (
32 + NetworkConnectorsSubscription,
33 +)
34 +from app.network_connectors.schema import AuthKey
35 +from app.network_connectors.schema import AvailableNetworkConnectorsResponse
36 +from app.network_connectors.schema import CreateNetworkConnectorsAuthKeys
37 +from app.network_connectors.schema import CreateNetworkConnectorsService
38 +from app.network_connectors.schema import CustomerNetworkConnectorsCreate
39 +from app.network_connectors.schema import CustomerNetworkConnectorsCreateResponse
40 +from app.network_connectors.schema import CustomerNetworkConnectorsDeleteResponse
41 +from app.network_connectors.schema import CustomerNetworkConnectorsMetaResponse
42 +from app.network_connectors.schema import CustomerNetworkConnectorsMetaSchema
43 +from app.network_connectors.schema import CustomerNetworkConnectorsResponse
44 +from app.network_connectors.schema import DeleteCustomerNetworkConnectors
45 +from app.network_connectors.schema import NetworkConnectorsWithAuthKeys
46 +from app.network_connectors.schema import UpdateCustomerNetworkConnectors
47 +
48 +network_connector_settings_router = APIRouter()
49 +
50 +
51 +async def fetch_available_network_connectors(session: AsyncSession):
52 + """
53 + Fetches available network_connectors and their auth keys from the database.
54 +
55 + Args:
56 + session (AsyncSession): The database session.
57 +
58 + Returns:
59 + List[NetworkConnectorsWithAuthKeys]: A list of available network_connectors with their auth keys.
60 + """
61 + stmt = select(AvailableNetworkConnectors).options(
62 + joinedload(AvailableNetworkConnectors.network_connector_keys),
63 + )
64 + result = await session.execute(stmt)
65 +
66 + # Use unique() to avoid duplicates caused by joined eager loading
67 + unique_network_connectors = result.unique().scalars().all()
68 +
69 + network_connectors_with_auth_keys = []
70 + for network_connector in unique_network_connectors:
71 + auth_keys = [AuthKey(auth_key_name=key.auth_key_name) for key in network_connector.network_connector_keys]
72 + network_connector_data = NetworkConnectorsWithAuthKeys(
73 + id=network_connector.id,
74 + network_connector_name=network_connector.network_connector_name,
75 + description=network_connector.description,
76 + network_connector_details=network_connector.network_connector_details,
77 + network_connector_keys=auth_keys,
78 + )
79 + network_connectors_with_auth_keys.append(network_connector_data)
80 +
81 + return network_connectors_with_auth_keys
82 +
83 +
84 +async def validate_network_connector_name(network_connector_name: str, session: AsyncSession):
85 + """
86 + Validate if the network_connector name exists in available network_connectors.
87 + """
88 + available_network_connectors = await fetch_available_network_connectors(session)
89 + if network_connector_name not in [ai.network_connector_name for ai in available_network_connectors]:
90 + raise HTTPException(
91 + status_code=400,
92 + detail=f"NetworkConnectors {network_connector_name} is not a valid network_connector.",
93 + )
94 +
95 +
96 +async def validate_network_connector_auth_keys(
97 + network_connector_name: str,
98 + network_connector_auth_keys: List[AuthKey],
99 + session: AsyncSession,
100 +):
101 + """
102 + Validate if the network_connector auth keys are valid.
103 + """
104 + available_network_connectors = await fetch_available_network_connectors(session)
105 + network_connector = [ai for ai in available_network_connectors if ai.network_connector_name == network_connector_name][0]
106 + available_auth_keys = [ak.auth_key_name for ak in network_connector.network_connector_keys]
107 + # loop through the `available_auth_keys` and check if the `network_connector_auth_keys` contains the `auth_key_name`
108 + for auth_key in available_auth_keys:
109 + if auth_key not in [iak.auth_key_name for iak in network_connector_auth_keys]:
110 + raise HTTPException(
111 + status_code=400,
112 + detail=f"NetworkConnectors auth key {auth_key} does not exist.",
113 + )
114 +
115 +
116 +async def validate_network_connector_auth_key_update(
117 + network_connector_name: str,
118 + network_connector_auth_key: List[AuthKey],
119 + session: AsyncSession,
120 +):
121 + """
122 + Validate if the network_connector auth key is valid.
123 + """
124 + logger.info(f"network_connector_auth_key: {network_connector_auth_key}")
125 + available_network_connectors = await fetch_available_network_connectors(session)
126 + network_connector = [ai for ai in available_network_connectors if ai.network_connector_name == network_connector_name][0]
127 + available_auth_keys = [ak.auth_key_name for ak in network_connector.auth_keys]
128 + for auth_key in network_connector_auth_key:
129 + if auth_key.auth_key_name not in available_auth_keys:
130 + raise HTTPException(
131 + status_code=400,
132 + detail=f"NetworkConnectors auth key {auth_key.auth_key_name} does not exist.",
133 + )
134 +
135 +
136 +async def validate_customer_code(customer_code: str, session: AsyncSession):
137 + """
138 + Validate if the customer code exists in the customers table.
139 + """
140 + stmt = select(Customers).where(Customers.customer_code == customer_code)
141 + result = await session.execute(stmt)
142 + if result.scalars().first() is None:
143 + raise HTTPException(
144 + status_code=400,
145 + detail=f"Customer {customer_code} does not exist.",
146 + )
147 +
148 +
149 +async def validate_customer_meta(customer_code: str, session: AsyncSession):
150 + """
151 + Validate if the customer code exists in the customers_meta table.
152 + """
153 + stmt = select(CustomersMeta).where(CustomersMeta.customer_code == customer_code)
154 + result = await session.execute(stmt)
155 + if result.scalars().first() is None:
156 + raise HTTPException(
157 + status_code=400,
158 + detail=f"Customer {customer_code} meta does not exist. Please provision the customer before creating an network_connector.",
159 + )
160 +
161 +
162 +async def check_existing_customer_network_connector(
163 + customer_code: str,
164 + network_connector_name: str,
165 + session: AsyncSession,
166 +):
167 + """
168 + Check if the customer network_connector already exists.
169 + """
170 + # Assuming NetworkConnectorsService has an 'network_connector_name' field or similar
171 + stmt = (
172 + select(CustomerNetworkConnectors)
173 + .join(CustomerNetworkConnectors.network_connectors_subscriptions)
174 + .join(NetworkConnectorsSubscription.network_connectors_service)
175 + .where(
176 + CustomerNetworkConnectors.customer_code == customer_code,
177 + NetworkConnectorsService.service_name == network_connector_name,
178 + )
179 + )
180 + result = await session.execute(stmt)
181 + if result.scalars().first() is not None:
182 + raise HTTPException(
183 + status_code=400,
184 + detail=f"Customer network_connector {customer_code} {network_connector_name} already exists.",
185 + )
186 +
187 +
188 +async def check_existing_customer_network_connector_meta(
189 + customer_code: str,
190 + network_connector_name: str,
191 + session: AsyncSession,
192 +):
193 + """
194 + Check if the customer network_connector meta already exists for the customer code and network_connector name.
195 + """
196 + stmt = select(CustomerNetworkConnectorsMeta).where(
197 + CustomerNetworkConnectorsMeta.customer_code == customer_code,
198 + CustomerNetworkConnectorsMeta.network_connector_name == network_connector_name,
199 + )
200 + result = await session.execute(stmt)
201 + if result.scalars().first() is not None:
202 + raise HTTPException(
203 + status_code=400,
204 + detail=f"Customer network_connector meta {customer_code} {network_connector_name} already exists.",
205 + )
206 +
207 +
208 +async def create_network_connector_service(
209 + network_connector_name: str,
210 + settings: CreateNetworkConnectorsService,
211 + session: AsyncSession,
212 +) -> NetworkConnectorsService:
213 + """
214 + Create or fetch NetworkConnectorsService instance with custom configuration.
215 + """
216 + network_connector_service = NetworkConnectorsService(
217 + service_name=network_connector_name,
218 + auth_type=settings.auth_type,
219 + configs=[
220 + NetworkConnectorsConfig(
221 + config_key=settings.config_key,
222 + config_value=settings.config_value,
223 + ),
224 + ],
225 + )
226 + session.add(network_connector_service)
227 + await session.flush()
228 + return network_connector_service
229 +
230 +
231 +async def create_customer_network_connectors(
232 + customer_code: str,
233 + customer_name: str,
234 + network_connector_service_id: int,
235 + network_connector_service_name: str,
236 + session: AsyncSession,
237 +) -> CustomerNetworkConnectors:
238 + """
239 + Create CustomerNetworkConnectors instance.
240 + """
241 + customer_network_connectors = CustomerNetworkConnectors(
242 + customer_code=customer_code,
243 + customer_name=customer_name,
244 + network_connector_service_id=network_connector_service_id,
245 + network_connector_service_name=network_connector_service_name,
246 + deployed=False,
247 + )
248 + session.add(customer_network_connectors)
249 + await session.flush()
250 + return customer_network_connectors
251 +
252 +
253 +async def create_network_connector_subscription(
254 + customer_network_connectors: CustomerNetworkConnectors,
255 + network_connector_service: NetworkConnectorsService,
256 + network_connector_auth_keys: List[CreateNetworkConnectorsAuthKeys],
257 + session: AsyncSession,
258 +):
259 + """
260 + Create NetworkConnectorsSubscription instance.
261 + """
262 + for auth_key in network_connector_auth_keys:
263 + new_network_connector_subscription = NetworkConnectorsSubscription(
264 + customer_network_connectors=customer_network_connectors,
265 + network_connectors_service=network_connector_service,
266 + network_connectors_keys=[
267 + NetworkConnectorsKeys(
268 + auth_key_name=auth_key.auth_key_name,
269 + auth_value=auth_key.auth_value,
270 + ),
271 + ],
272 + )
273 + session.add(new_network_connector_subscription)
274 + await session.commit()
275 +
276 +
277 +async def get_customer_and_service_ids(session, customer_code, network_connector_name):
278 + try:
279 + result = await session.execute(
280 + select(CustomerNetworkConnectors.id, NetworkConnectorsService.id)
281 + .join(
282 + NetworkConnectorsSubscription,
283 + CustomerNetworkConnectors.id == NetworkConnectorsSubscription.customer_id,
284 + )
285 + .join(
286 + NetworkConnectorsService,
287 + NetworkConnectorsSubscription.network_connectors_service_id == NetworkConnectorsService.id,
288 + )
289 + .where(
290 + CustomerNetworkConnectors.customer_code == customer_code,
291 + NetworkConnectorsService.service_name == network_connector_name,
292 + ),
293 + )
294 + return result.all()
295 + except NoResultFound:
296 + raise HTTPException(status_code=404, detail="Customer network_connector not found")
297 +
298 +
299 +async def get_subscription_ids(session, customer_id, network_connector_service_id):
300 + result = await session.execute(
301 + select(NetworkConnectorsSubscription.id).where(
302 + NetworkConnectorsSubscription.customer_id == customer_id,
303 + NetworkConnectorsSubscription.network_connectors_service_id == network_connector_service_id,
304 + ),
305 + )
306 + # Fetch all results
307 + subscription_ids_raw = result.scalars().all()
308 +
309 + # Process the results
310 + # If the result is a list of tuples (even with one element), extract the first element
311 + if subscription_ids_raw and isinstance(subscription_ids_raw[0], tuple):
312 + return [id_tuple[0] for id_tuple in subscription_ids_raw]
313 + # If the result is a list of integers
314 + elif subscription_ids_raw and isinstance(subscription_ids_raw[0], int):
315 + return subscription_ids_raw
316 + # If there are no results
317 + else:
318 + return []
319 +
320 +
321 +async def delete_metadata(session, subscription_ids):
322 + await session.execute(
323 + delete(NetworkConnectorsKeys).where(
324 + NetworkConnectorsKeys.subscription_id.in_(subscription_ids),
325 + ),
326 + )
327 +
328 +
329 +async def delete_subscriptions(session, subscription_ids):
330 + await session.execute(
331 + delete(NetworkConnectorsSubscription).where(
332 + NetworkConnectorsSubscription.id.in_(subscription_ids),
333 + ),
334 + )
335 +
336 +
337 +async def delete_configs(session, network_connector_service_id):
338 + await session.execute(
339 + delete(NetworkConnectorsConfig).where(
340 + NetworkConnectorsConfig.network_connector_service_id == network_connector_service_id,
341 + ),
342 + )
343 +
344 +
345 +async def delete_network_connector_service(session, network_connector_service_id):
346 + await session.execute(
347 + delete(NetworkConnectorsService).where(
348 + NetworkConnectorsService.id == network_connector_service_id,
349 + ),
350 + )
351 +
352 +
353 +async def delete_customer_network_connector_record(session, customer_id):
354 + await session.execute(
355 + delete(CustomerNetworkConnectors).where(CustomerNetworkConnectors.id == customer_id),
356 + )
357 +
358 +
359 +async def find_customer_network_connector(
360 + customer_code: str,
361 + network_connector_name: str,
362 + customer_network_connector_response,
363 +) -> Optional[CustomerNetworkConnectors]:
364 + for ci in customer_network_connector_response.available_network_connectors:
365 + for subscription in ci.network_connectors_subscriptions:
366 + if subscription.network_connectors_service.service_name == network_connector_name:
367 + return ci
368 + return None
369 +
370 +
371 +def get_subscription_id(
372 + customer_network_connector,
373 + network_connector_name: str,
374 + auth_key_name: str,
375 +) -> Optional[int]:
376 + logger.info(f"Getting subscription id for {network_connector_name} {auth_key_name}")
377 + for subscription in customer_network_connector.network_connectors_subscriptions:
378 + if subscription.network_connectors_service.service_name == network_connector_name:
379 + for auth_key in subscription.network_connector_keys:
380 + if auth_key.auth_key_name == auth_key_name:
381 + return subscription.id
382 + return None
383 +
384 +
385 +async def get_tenant_id(
386 + customer_network_connector: CustomerNetworkConnectorsCreate,
387 + session: AsyncSession,
388 +) -> str:
389 + """
390 + Retrieves the Tenant ID for a given customer network_connector. This is the Office365 organization ID and
391 + is used to create alerts for the customer in DFIR-IRIS.
392 + """
393 + stmt = (
394 + select(NetworkConnectorsKeys)
395 + .join(
396 + NetworkConnectorsSubscription,
397 + NetworkConnectorsKeys.subscription_id == NetworkConnectorsSubscription.id,
398 + )
399 + .join(
400 + CustomerNetworkConnectors,
401 + NetworkConnectorsSubscription.customer_id == CustomerNetworkConnectors.id,
402 + )
403 + .join(
404 + NetworkConnectorsService,
405 + NetworkConnectorsSubscription.network_connector_service_id == NetworkConnectorsService.id,
406 + )
407 + .where(
408 + CustomerNetworkConnectors.customer_code == customer_network_connector.customer_code,
409 + NetworkConnectorsService.service_name == customer_network_connector.network_connector_name,
410 + NetworkConnectorsKeys.auth_key_name == "TENANT_ID",
411 + )
412 + )
413 +
414 + result = await session.execute(stmt)
415 + tenant_id = result.scalars().first()
416 + if tenant_id is None:
417 + raise HTTPException(
418 + status_code=404,
419 + detail=f"Tenant ID for customer {customer_network_connector.customer_code} not found.",
420 + )
421 + logger.info(f"tenant_id: {tenant_id.auth_value}")
422 + return tenant_id.auth_value
423 +
424 +
425 +async def update_office365_organization_id(
426 + customer_code: str,
427 + tenant_id: str,
428 + session: AsyncSession,
429 +):
430 + """
431 + Updates the Office365 organization ID in the alert_creation_settings table.
432 + """
433 + stmt = (
434 + update(AlertCreationSettings)
435 + .where(AlertCreationSettings.customer_code == customer_code)
436 + .values(office365_organization_id=tenant_id)
437 + )
438 + await session.execute(stmt)
439 + await session.commit()
440 +
441 +
442 +async def get_network_connector_service_id(
443 + network_connector_name: str,
444 + session: AsyncSession,
445 +) -> int:
446 + """
447 + Retrieves the AvailableNetworkConnectorss ID for a given network_connector name.
448 + """
449 + stmt = select(AvailableNetworkConnectors).where(
450 + AvailableNetworkConnectors.network_connector_name == network_connector_name,
451 + )
452 + result = await session.execute(stmt)
453 + network_connector_service = result.scalars().first()
454 + if network_connector_service is None:
455 + raise HTTPException(
456 + status_code=404,
457 + detail=f"NetworkConnectors service {network_connector_name} not found.",
458 + )
459 + return network_connector_service.id
460 +
461 +
462 +async def get_network_connector_service_name(
463 + network_connector_name: str,
464 + session: AsyncSession,
465 +) -> str:
466 + """
467 + Retrieves the AvailableNetworkConnectors ID for a given network_connector name.
468 + """
469 + stmt = select(AvailableNetworkConnectors).where(
470 + AvailableNetworkConnectors.network_connector_name == network_connector_name,
471 + )
472 + result = await session.execute(stmt)
473 + network_connector_service = result.scalars().first()
474 + if network_connector_service is None:
475 + raise HTTPException(
476 + status_code=404,
477 + detail=f"NetworkConnectors service {network_connector_name} not found.",
478 + )
479 + return network_connector_service.network_connector_name
480 +
481 +
482 +async def fetch_customer_network_connectors_data(session: AsyncSession):
483 + """
484 + Fetches customer network_connectors data from the database.
485 + """
486 + stmt = select(CustomerNetworkConnectors).options(
487 + joinedload(CustomerNetworkConnectors.network_connectors_subscriptions).joinedload(
488 + NetworkConnectorsSubscription.network_connectors_service,
489 + ),
490 + joinedload(CustomerNetworkConnectors.network_connectors_subscriptions).subqueryload(
491 + NetworkConnectorsSubscription.network_connectors_keys,
492 + ),
493 + )
494 + result = await session.execute(stmt)
495 + return result.scalars().unique().all()
496 +
497 +
498 +def process_customer_network_connectors(customer_network_connectors_data):
499 + """
500 + Processes customer network_connectors data and returns a list of CustomerNetworkConnectors objects.
501 + """
502 + processed_customer_network_connectors = []
503 + for ci in customer_network_connectors_data:
504 + first_service_id = (
505 + ci.network_connectors_subscriptions[0].network_connectors_service_id if ci.network_connectors_subscriptions else None
506 + )
507 + customer_network_connector_obj = CustomerNetworkConnectors(
508 + id=ci.id,
509 + customer_code=ci.customer_code,
510 + customer_name=ci.customer_name,
511 + network_connectors_subscriptions=ci.network_connectors_subscriptions,
512 + network_connector_service_id=first_service_id,
513 + network_connector_service_name=ci.network_connectors_subscriptions[0].network_connectors_service.service_name
514 + if ci.network_connectors_subscriptions
515 + else None,
516 + deployed=ci.deployed,
517 + )
518 + processed_customer_network_connectors.append(customer_network_connector_obj)
519 + return processed_customer_network_connectors
520 +
521 +
522 +@network_connector_settings_router.get(
523 + "/available_network_connectors",
524 + response_model=AvailableNetworkConnectorsResponse,
525 + description="Get a list of available network_connectors.",
526 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
527 +)
528 +async def get_available_network_connectors(
529 + session: AsyncSession = Depends(get_db),
530 +):
531 + """
532 + Endpoint to get a list of available network_connectors.
533 + """
534 + available_network_connectors = await fetch_available_network_connectors(session)
535 + return AvailableNetworkConnectorsResponse(
536 + network_connector_keys=available_network_connectors,
537 + message="Available network_connectors successfully retrieved.",
538 + success=True,
539 + )
540 +
541 +
542 +@network_connector_settings_router.get(
543 + "/customer_network_connectors",
544 + response_model=CustomerNetworkConnectorsResponse,
545 + description="Get a list of customer network_connectors.",
546 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
547 +)
548 +async def get_customer_network_connectors(session: AsyncSession = Depends(get_db)):
549 + """
550 + Endpoint to get a list of customer network_connectors.
551 + """
552 + customer_network_connectors_data = await fetch_customer_network_connectors_data(session)
553 + processed_customer_network_connectors = process_customer_network_connectors(
554 + customer_network_connectors_data,
555 + )
556 +
557 + return CustomerNetworkConnectorsResponse(
558 + available_network_connectors=processed_customer_network_connectors,
559 + message="Customer network_connectors successfully retrieved.",
560 + success=True,
561 + )
562 +
563 +
564 +@network_connector_settings_router.get(
565 + "/customer_network_connectors_meta",
566 + response_model=CustomerNetworkConnectorsMetaResponse,
567 + description="Get a list of customer network_connectors metadata.",
568 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
569 +)
570 +async def get_customer_network_connectors_meta(session: AsyncSession = Depends(get_db)):
571 + """
572 + Endpoint to get a list of customer network_connectors metadata.
573 + """
574 + try:
575 + stmt = select(CustomerNetworkConnectorsMeta)
576 + result = await session.execute(stmt)
577 + customer_network_connectors_meta = result.scalars().all()
578 + except Exception as e:
579 + logger.error(f"Error while fetching customer network_connectors metadata: {e}")
580 + customer_network_connectors_meta = []
581 +
582 + logger.info(f"customer_network_connectors_meta: {customer_network_connectors_meta}")
583 + return CustomerNetworkConnectorsMetaResponse(
584 + customer_network_connectors_meta=customer_network_connectors_meta,
585 + message="Customer network_connectors metadata successfully retrieved.",
586 + success=True,
587 + )
588 +
589 +
590 +@network_connector_settings_router.get(
591 + "/customer_network_connectors/{customer_code}",
592 + response_model=CustomerNetworkConnectorsResponse,
593 + description="Get a list of customer network_connectors for a specific customer.",
594 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
595 +)
596 +async def get_customer_network_connectors_by_customer_code(
597 + customer_code: str,
598 + session: AsyncSession = Depends(get_db),
599 +):
600 + """
601 + Endpoint to get a list of customer network_connectors for a specific customer.
602 + """
603 + stmt = (
604 + select(CustomerNetworkConnectors)
605 + .options(
606 + joinedload(CustomerNetworkConnectors.network_connectors_subscriptions).joinedload(
607 + NetworkConnectorsSubscription.network_connectors_service,
608 + ),
609 + joinedload(CustomerNetworkConnectors.network_connectors_subscriptions).subqueryload(
610 + NetworkConnectorsSubscription.network_connectors_keys,
611 + ), # Load NetworkConnectorsAuthKeys
612 + )
613 + .where(CustomerNetworkConnectors.customer_code == customer_code)
614 + )
615 + result = await session.execute(stmt)
616 + customer_network_connectors = result.scalars().unique().all()
617 + return CustomerNetworkConnectorsResponse(
618 + available_network_connectors=customer_network_connectors,
619 + message="Customer network_connectors successfully retrieved.",
620 + success=True,
621 + )
622 +
623 +
624 +@network_connector_settings_router.get(
625 + "/customer_network_connectors_meta/{customer_code}",
626 + response_model=CustomerNetworkConnectorsMetaResponse,
627 + description="Get a list of customer network_connectors metadata for a specific customer.",
628 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
629 +)
630 +async def get_customer_network_connectors_meta_by_customer_code(
631 + customer_code: str,
632 + session: AsyncSession = Depends(get_db),
633 +):
634 + """
635 + Endpoint to get a list of customer network_connectors metadata for a specific customer.
636 + """
637 + stmt = select(CustomerNetworkConnectorsMeta).where(
638 + CustomerNetworkConnectorsMeta.customer_code == customer_code,
639 + )
640 + result = await session.execute(stmt)
641 + customer_network_connectors_meta = result.scalars().all()
642 + logger.info(f"customer_network_connectors_meta: {customer_network_connectors_meta}")
643 + return CustomerNetworkConnectorsMetaResponse(
644 + customer_network_connectors_meta=customer_network_connectors_meta,
645 + message="Customer network_connectors metadata successfully retrieved.",
646 + success=True,
647 + )
648 +
649 +
650 +@network_connector_settings_router.post(
651 + "/create_network_connector",
652 + response_model=CustomerNetworkConnectorsCreateResponse,
653 + description="Create a new customer network_connector.",
654 + # dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
655 +)
656 +async def create_network_connector(
657 + customer_network_connector_create: CustomerNetworkConnectorsCreate,
658 + session: AsyncSession = Depends(get_db),
659 +):
660 + """
661 + Endpoint to create a new customer network_connector.
662 + """
663 + await validate_network_connector_name(
664 + customer_network_connector_create.network_connector_name,
665 + session,
666 + )
667 + await validate_network_connector_auth_keys(
668 + customer_network_connector_create.network_connector_name,
669 + customer_network_connector_create.network_connector_auth_keys,
670 + session,
671 + )
672 + await validate_customer_code(customer_network_connector_create.customer_code, session)
673 + await validate_customer_meta(customer_network_connector_create.customer_code, session)
674 + await check_existing_customer_network_connector(
675 + customer_network_connector_create.customer_code,
676 + customer_network_connector_create.network_connector_name,
677 + session,
678 + )
679 + network_connector_service_id = await get_network_connector_service_id(
680 + customer_network_connector_create.network_connector_name,
681 + session,
682 + )
683 + network_connector_service_name = await get_network_connector_service_name(
684 + customer_network_connector_create.network_connector_name,
685 + session,
686 + )
687 +
688 + network_connector_service = await create_network_connector_service(
689 + customer_network_connector_create.network_connector_name,
690 + settings=customer_network_connector_create.network_connector_config,
691 + session=session,
692 + )
693 + customer_network_connectors = await create_customer_network_connectors(
694 + customer_network_connector_create.customer_code,
695 + customer_network_connector_create.customer_name,
696 + network_connector_service_id=network_connector_service_id,
697 + network_connector_service_name=network_connector_service_name,
698 + session=session,
699 + )
700 + logger.info("Getting customer network_connector auth keys for subscription creation.")
701 + logger.info(f"Customer Network Connectors: {customer_network_connectors}")
702 + await create_network_connector_subscription(
703 + customer_network_connectors,
704 + network_connector_service,
705 + network_connector_auth_keys=customer_network_connector_create.network_connector_auth_keys,
706 + session=session,
707 + )
708 +
709 + return CustomerNetworkConnectorsCreateResponse(
710 + message=f"Customer network_connector {customer_network_connector_create.customer_code} {customer_network_connector_create.network_connector_name} successfully created.",
711 + success=True,
712 + )
713 +
714 +
715 +@network_connector_settings_router.post(
716 + "/create_network_connector_meta",
717 + response_model=CustomerNetworkConnectorsMetaResponse,
718 + description="Create a new customer network_connector metadata.",
719 + # dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
720 +)
721 +async def create_network_connector_meta(
722 + customer_network_connector_meta: CustomerNetworkConnectorsMetaSchema,
723 + session: AsyncSession = Depends(get_db),
724 +):
725 + """
726 + Endpoint to create a new customer network_connector metadata.
727 + """
728 + await validate_customer_code(customer_network_connector_meta.customer_code, session)
729 + await validate_customer_meta(customer_network_connector_meta.customer_code, session)
730 + await check_existing_customer_network_connector_meta(
731 + customer_network_connector_meta.customer_code,
732 + customer_network_connector_meta.network_connector_name,
733 + session,
734 + )
735 + try:
736 + new_customer_network_connector_meta = CustomerNetworkConnectorsMeta(
737 + **customer_network_connector_meta.dict(),
738 + )
739 + session.add(new_customer_network_connector_meta)
740 + await session.commit()
741 + return CustomerNetworkConnectorsMetaResponse(
742 + message="Customer network_connector metadata successfully created.",
743 + success=True,
744 + )
745 + except Exception as e:
746 + logger.error(f"Error while creating customer network_connector metadata: {e}")
747 + return CustomerNetworkConnectorsMetaResponse(
748 + customer_network_connectors_meta=None,
749 + message="Error while creating customer network_connector metadata.",
750 + success=False,
751 + )
752 +
753 +
754 +@network_connector_settings_router.put(
755 + "/update_network_connector/{customer_code}",
756 + response_model=CustomerNetworkConnectorsCreateResponse,
757 + description="Update a customer network_connector.",
758 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
759 +)
760 +async def update_network_connector(
761 + customer_code: str,
762 + customer_network_connector_update: UpdateCustomerNetworkConnectors,
763 + session: AsyncSession = Depends(get_db),
764 +):
765 + await validate_network_connector_name(
766 + customer_network_connector_update.network_connector_name,
767 + session,
768 + )
769 + customer_network_connector_response = await get_customer_network_connectors_by_customer_code(
770 + customer_code,
771 + session,
772 + )
773 +
774 + if not customer_network_connector_response:
775 + raise HTTPException(status_code=404, detail="Customer network_connectors not found")
776 +
777 + customer_network_connector = await find_customer_network_connector(
778 + customer_code,
779 + customer_network_connector_update.network_connector_name,
780 + customer_network_connector_response,
781 + )
782 +
783 + if not customer_network_connector:
784 + raise HTTPException(
785 + status_code=404,
786 + detail="Customer network_connector with specified service name not found.",
787 + )
788 +
789 + await validate_network_connector_auth_key_update(
790 + customer_network_connector_update.network_connector_name,
791 + customer_network_connector_update.network_connector_auth_keys,
792 + session,
793 + )
794 +
795 + subscription_id = get_subscription_id(
796 + customer_network_connector,
797 + customer_network_connector_update.network_connector_name,
798 + customer_network_connector_update.network_connector_auth_keys[0].auth_key_name,
799 + )
800 +
801 + if not subscription_id:
802 + raise HTTPException(
803 + status_code=404,
804 + detail=f"NetworkConnectors auth key {customer_network_connector_update.network_connector_auth_keys[0].auth_key_name} not found.",
805 + )
806 +
807 + await session.execute(
808 + update(NetworkConnectorsKeys)
809 + .where(NetworkConnectorsKeys.subscription_id == subscription_id)
810 + .values(
811 + auth_value=customer_network_connector_update.network_connector_auth_keys[0].auth_value,
812 + ),
813 + )
814 +
815 + await session.commit()
816 +
817 + return CustomerNetworkConnectorsCreateResponse(
818 + message=f"Customer network_connector {customer_code} {customer_network_connector_update.network_connector_name} successfully updated.",
819 + success=True,
820 + )
821 +
822 +
823 +@network_connector_settings_router.put(
824 + "/available_network_connectors",
825 + response_model=AvailableNetworkConnectorsResponse,
826 + description="Update an available network_connector.",
827 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
828 +)
829 +async def update_available_network_connectors(
830 + available_network_connectors: List[AvailableNetworkConnectors],
831 + session: AsyncSession = Depends(get_db),
832 +):
833 + """
834 + Endpoint to update an available network_connector.
835 + """
836 + for network_connector in available_network_connectors:
837 + stmt = select(AvailableNetworkConnectors).where(
838 + AvailableNetworkConnectors.network_connector_name == network_connector.network_connector_name,
839 + )
840 + result = await session.execute(stmt)
841 + existing_network_connector = result.scalars().first()
842 +
843 + if existing_network_connector is None:
844 + raise HTTPException(
845 + status_code=404,
846 + detail=f"NetworkConnectors {network_connector.network_connector_name} not found.",
847 + )
848 +
849 + existing_network_connector.description = network_connector.description
850 + existing_network_connector.network_connector_details = network_connector.network_connector_details
851 +
852 + await session.commit()
853 +
854 + return AvailableNetworkConnectorsResponse(
855 + available_network_connectors=available_network_connectors,
856 + message="Available network_connectors successfully updated.",
857 + success=True,
858 + )
859 +
860 +
861 +@network_connector_settings_router.delete(
862 + "/delete_network_connector",
863 + response_model=CustomerNetworkConnectorsDeleteResponse,
864 + description="Delete a customer network_connector.",
865 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
866 +)
867 +async def delete_network_connector(
868 + delete_customer_network_connector: DeleteCustomerNetworkConnectors,
869 + session: AsyncSession = Depends(get_db),
870 +):
871 + customer_code = delete_customer_network_connector.customer_code
872 + network_connector_name = delete_customer_network_connector.network_connector_name
873 +
874 + results = await get_customer_and_service_ids(
875 + session,
876 + customer_code,
877 + network_connector_name,
878 + )
879 + # Check if results is not empty
880 + if results:
881 + # Unpack the first tuple in results
882 + customer_id, network_connector_service_id = results[0]
883 + else:
884 + # Handle the case where results is empty
885 + raise HTTPException(status_code=404, detail="Customer network_connector not found")
886 +
887 + subscription_ids = await get_subscription_ids(
888 + session,
889 + customer_id,
890 + network_connector_service_id,
891 + )
892 + if not subscription_ids:
893 + raise HTTPException(
894 + status_code=404,
895 + detail="No subscriptions found for customer network_connector",
896 + )
897 +
898 + await delete_metadata(session, subscription_ids)
899 + await delete_subscriptions(session, subscription_ids)
900 + await delete_configs(session, network_connector_service_id)
901 + await delete_network_connector_service(session, network_connector_service_id)
902 + await delete_customer_network_connector_record(session, customer_id)
903 +
904 + await session.commit()
905 +
906 + return CustomerNetworkConnectorsDeleteResponse(
907 + message=f"Customer network_connector {customer_code} {network_connector_name} successfully deleted.",
908 + success=True,
909 + )
910 +
911 +
912 +@network_connector_settings_router.delete(
913 + "/delete_network_connector_meta",
914 + response_model=CustomerNetworkConnectorsMetaResponse,
915 + description="Delete a customer network_connector metadata.",
916 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
917 +)
918 +async def delete_network_connector_meta(
919 + customer_network_connector_meta: CustomerNetworkConnectorsMetaSchema,
920 + session: AsyncSession = Depends(get_db),
921 +):
922 + """
923 + Endpoint to delete a customer network_connector metadata.
924 + """
925 + try:
926 + stmt = delete(CustomerNetworkConnectorsMeta).where(
927 + CustomerNetworkConnectorsMeta.customer_code == customer_network_connector_meta.customer_code,
928 + CustomerNetworkConnectorsMeta.network_connector_name == customer_network_connector_meta.network_connector_name,
929 + )
930 + await session.execute(stmt)
931 + await session.commit()
932 + return CustomerNetworkConnectorsMetaResponse(
933 + message="Customer network_connector metadata successfully deleted.",
934 + success=True,
935 + )
936 + except Exception as e:
937 + logger.error(f"Error while deleting customer network_connector metadata: {e}")
938 + return CustomerNetworkConnectorsMetaResponse(
939 + customer_network_connectors_meta=None,
940 + message="Error while deleting customer network_connector metadata.",
941 + success=False,
942 + )
backend/app/network_connectors/schema.py new
+233
@@ -0,0 +1,233 @@
1 +from typing import List
2 +from typing import Optional
3 +
4 +from pydantic import BaseModel
5 +from pydantic import Field
6 +
7 +
8 +class AuthKey(BaseModel):
9 + auth_key_name: str
10 +
11 +
12 +class NetworkConnectorsWithAuthKeys(BaseModel):
13 + id: int
14 + network_connector_name: str
15 + description: str
16 + network_connector_details: str
17 + network_connector_keys: List[AuthKey]
18 +
19 +
20 +class AvailableNetworkConnectorsResponse(BaseModel):
21 + network_connector_keys: List[NetworkConnectorsWithAuthKeys]
22 + message: str
23 + success: bool
24 +
25 +
26 +class CreateNetworkConnectorsService(BaseModel):
27 + auth_type: str = Field(
28 + ...,
29 + description="The authentication type.",
30 + examples=["OAuth"],
31 + )
32 + config_key: str = Field(
33 + ...,
34 + description="The configuration key.",
35 + examples=["endpoint"],
36 + )
37 + config_value: str = Field(
38 + ...,
39 + description="The configuration value.",
40 + examples=["https://api.mimecast.com"],
41 + )
42 +
43 +
44 +class CreateNetworkConnectorsAuthKeys(BaseModel):
45 + auth_key_name: str = Field(
46 + ...,
47 + description="The auth key.",
48 + examples=["username"],
49 + )
50 + auth_value: str = Field(
51 + ...,
52 + description="The auth value.",
53 + examples=["test-user"],
54 + )
55 +
56 +
57 +class CustomerNetworkConnectorsCreate(BaseModel):
58 + customer_code: str = Field(
59 + ...,
60 + description="The customer code.",
61 + examples=["00002"],
62 + )
63 + customer_name: str = Field(
64 + ...,
65 + description="The customer name.",
66 + examples=["SOCFortress"],
67 + )
68 + network_connector_name: str = Field(
69 + ...,
70 + description="The integration name.",
71 + examples=["Mimecast"],
72 + )
73 + network_connector_config: CreateNetworkConnectorsService = Field(
74 + ...,
75 + description="The integration service.",
76 + examples=[{"auth_type": "OAuth", "config_key": "endpoint", "config_value": "https://api.mimecast.com"}],
77 + )
78 + # network_connector_auth_key: CreateIntegrationAuthKeys = Field(
79 + # ...,
80 + # description="The integration metadata.",
81 + # )
82 + network_connector_auth_keys: List[CreateNetworkConnectorsAuthKeys] = Field(
83 + ...,
84 + description="The integration auth keys.",
85 + )
86 +
87 +
88 +class CustomerNetworkConnectorsCreateResponse(BaseModel):
89 + message: str = Field(
90 + ...,
91 + description="The message.",
92 + )
93 + success: bool = Field(
94 + ...,
95 + description="The success status.",
96 + )
97 +
98 +
99 +class CustomerNetworkConnectorsDeleteResponse(BaseModel):
100 + message: str = Field(
101 + ...,
102 + description="The message.",
103 + )
104 + success: bool = Field(
105 + ...,
106 + description="The success status.",
107 + )
108 +
109 +
110 +# class IntegrationConfig(BaseModel):
111 +# config_id: int
112 +# config_value: str
113 +# config_key: str
114 +
115 +# class IntegrationService(BaseModel):
116 +# auth_type: str
117 +# service_name: str
118 +# id: int
119 +
120 +# class IntegrationSubscription(BaseModel):
121 +# id: int
122 +# customer_id: int
123 +# network_connector_service_id: int
124 +# network_connector_service: IntegrationService
125 +# network_connector_config: IntegrationConfig
126 +
127 +# class CustomerNetworkConnectors(BaseModel):
128 +# customer_code: str
129 +# id: int
130 +# customer_name: str
131 +# network_connector_subscriptions: List[IntegrationSubscription]
132 +
133 +# class CustomerNetworkConnectorsResponse(BaseModel):
134 +# available_integrations: List[CustomerNetworkConnectors]
135 +# message: str
136 +# success: bool
137 +
138 +
139 +class NetworkConnectorsAuthKeys(BaseModel):
140 + id: int
141 + auth_key_name: str
142 + auth_value: str
143 + subscription_id: int
144 +
145 +
146 +class NetworkConnectorsService(BaseModel):
147 + auth_type: str
148 + service_name: str
149 + id: int
150 +
151 +
152 +class NetworkConnectorsSubscription(BaseModel):
153 + id: int
154 + customer_id: int
155 + network_connectors_service_id: int
156 + network_connectors_service: NetworkConnectorsService
157 + network_connectors_keys: List[NetworkConnectorsAuthKeys]
158 +
159 +
160 +class CustomerNetworkConnectors(BaseModel):
161 + customer_code: str
162 + id: int
163 + customer_name: str
164 + network_connectors_subscriptions: List[NetworkConnectorsSubscription]
165 + network_connector_service_id: Optional[int] = Field(
166 + None,
167 + description="The integration service id.",
168 + examples=[1],
169 + )
170 + network_connector_service_name: Optional[str] = Field(
171 + ...,
172 + description="The integration service name.",
173 + examples=["Mimecast"],
174 + )
175 + deployed: Optional[bool] = Field(
176 + None,
177 + description="The deployment status.",
178 + examples=[True],
179 + )
180 +
181 +
182 +class CustomerNetworkConnectorsResponse(BaseModel):
183 + available_network_connectors: List[CustomerNetworkConnectors]
184 + message: str
185 + success: bool
186 +
187 +
188 +class DeleteCustomerNetworkConnectors(BaseModel):
189 + customer_code: str = Field(
190 + ...,
191 + description="The customer code.",
192 + examples=["00002"],
193 + )
194 + network_connector_name: str = Field(
195 + ...,
196 + description="The integration name.",
197 + examples=["Mimecast"],
198 + )
199 +
200 +
201 +class UpdateCustomerNetworkConnectors(BaseModel):
202 + network_connector_name: str = Field(
203 + ...,
204 + description="The integration name.",
205 + examples=["Mimecast"],
206 + )
207 + network_connector_auth_keys: List[CreateNetworkConnectorsAuthKeys] = Field(
208 + ...,
209 + description="The integration auth keys.",
210 + )
211 +
212 +
213 +class CustomerNetworkConnectorsMetaSchema(BaseModel):
214 + id: Optional[int] = None
215 + customer_code: str
216 + network_connector_name: str
217 + graylog_input_id: Optional[str] = None
218 + graylog_index_id: str
219 + graylog_stream_id: str
220 + grafana_org_id: str
221 + grafana_dashboard_folder_id: str
222 +
223 + class Config:
224 + orm_mode = True
225 +
226 +
227 +class CustomerNetworkConnectorsMetaResponse(BaseModel):
228 + message: str
229 + success: bool
230 + customer_network_connectors_meta: Optional[List[CustomerNetworkConnectorsMetaSchema]] = Field(
231 + None,
232 + description="The customer integrations metadata.",
233 + )
backend/app/routers/network_connectors.py new
+13
@@ -0,0 +1,13 @@
1 +from fastapi import APIRouter
2 +
3 +from app.network_connectors.routes import network_connector_settings_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the Inntegration Settings related routes
9 +router.include_router(
10 + network_connector_settings_router,
11 + prefix="/network_connectors",
12 + tags=["Network Connectors"],
13 +)
backend/app/routers/stack_provisioning.py
+10
@@ -1,5 +1,8 @@
1 from fastapi import APIRouter
2
3 +from app.stack_provisioning.graylog.routes.fortinet import (
4 + stack_provisioning_graylog_fortinet_router,
5 +)
6 from app.stack_provisioning.graylog.routes.provision import (
7 stack_provisioning_graylog_router,
8 )
@@ -13,3 +16,10 @@ router.include_router(
16 prefix="/stack_provisioning",
17 tags=["Stack Provisioning"],
18 )
19 +
20 +# Include the Stack Provisioning related routes
21 +router.include_router(
22 + stack_provisioning_graylog_fortinet_router,
23 + prefix="/stack_provisioning",
24 + tags=["Stack Provisioning"],
25 +)
backend/app/schedulers/models/scheduler.py
+1
@@ -14,6 +14,7 @@ class JobMetadata(SQLModel, table=True):
14 time_interval: int # The frequency of the job in minutes
15 extra_data: Optional[str] = None # Extra data for the job
16 enabled: bool # Indicates if the job is active or not
17 + job_description: Optional[str] = Field(max_length=1024) # Description of the job
18
19
20 class CreateSchedulerRequest(BaseModel):
backend/app/schedulers/routes/scheduler.py
+3
@@ -100,12 +100,15 @@ async def get_all_jobs(session: AsyncSession = Depends(get_db)) -> JobsResponse:
100 select(JobMetadata).filter_by(job_id=job.id),
101 )
102 job_metadata = job_metadata.scalars().first()
103 + logger.info(f"job_metadata: {job_metadata}")
104 apscheduler_jobs.append(
105 {
106 "id": job.id,
107 "name": job.name,
108 "time_interval": job_metadata.time_interval,
109 "enabled": job_metadata.enabled,
110 + "description": job_metadata.job_description,
111 + "last_success": job_metadata.last_success,
112 },
113 )
114 logger.info(f"apscheduler_jobs: {apscheduler_jobs}")
backend/app/schedulers/scheduler.py
+7 -1
@@ -117,7 +117,12 @@ async def initialize_job_metadata():
117 # Implement logic to initialize or update job metadata.
118 # Example: Check and add metadata for each known job
119 known_jobs = [
120 - {"job_id": "agent_sync", "time_interval": 15, "function": agent_sync},
120 + {
121 + "job_id": "agent_sync",
122 + "time_interval": 15,
123 + "function": agent_sync,
124 + "description": "Synchronizes agents with the Wazuh Manager and Velociraptor server.",
125 + },
126 # {"job_id": "invoke_mimecast_integration", "time_interval": 5, "function": invoke_mimecast_integration}
127 ]
128 for job in known_jobs:
@@ -131,6 +136,7 @@ async def initialize_job_metadata():
136 last_success=None,
137 time_interval=job["time_interval"],
138 enabled=True,
139 + job_description=job["description"],
140 )
141 session.add(job_metadata)
142 else:
backend/app/schedulers/schema/scheduler.py
+3
@@ -1,5 +1,6 @@
1 from datetime import datetime
2 from typing import List
3 +from typing import Optional
4
5 from pydantic import BaseModel
6
@@ -9,6 +10,8 @@ class Job(BaseModel):
10 name: str
11 enabled: bool
12 time_interval: int
13 + last_success: Optional[datetime]
14 + description: Optional[str]
15
16
17 class JobsResponse(BaseModel):
backend/app/stack_provisioning/graylog/routes/fortinet.py new
+133
@@ -0,0 +1,133 @@
1 +from typing import Dict
2 +
3 +from fastapi import APIRouter
4 +from fastapi import Depends
5 +from fastapi import HTTPException
6 +from fastapi import Security
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +
9 +from app.auth.utils import AuthHandler
10 +from app.db.db_session import get_db
11 +from app.network_connectors.routes import find_customer_network_connector
12 +from app.network_connectors.routes import (
13 + get_customer_network_connectors_by_customer_code,
14 +)
15 +from app.network_connectors.schema import CustomerNetworkConnectors
16 +from app.network_connectors.schema import CustomerNetworkConnectorsResponse
17 +from app.stack_provisioning.graylog.schema.fortinet import FortinetCustomerDetails
18 +from app.stack_provisioning.graylog.schema.fortinet import ProvisionFortinetKeys
19 +from app.stack_provisioning.graylog.schema.fortinet import ProvisionFortinetRequest
20 +from app.stack_provisioning.graylog.schema.fortinet import ProvisionFortinetResponse
21 +from app.stack_provisioning.graylog.services.fortinet import provision_fortinet
22 +
23 +stack_provisioning_graylog_fortinet_router = APIRouter()
24 +
25 +
26 +async def get_customer_integration_response(
27 + customer_code: str,
28 + session: AsyncSession,
29 +) -> CustomerNetworkConnectorsResponse:
30 + """
31 + Retrieves the integration response for a customer.
32 +
33 + Args:
34 + customer_code (str): The code of the customer.
35 + session (AsyncSession): The async session object for database operations.
36 +
37 + Returns:
38 + CustomerIntegrationsResponse: The integration response for the customer.
39 +
40 + Raises:
41 + HTTPException: If the customer integration settings are not found.
42 + """
43 + customer_integration_response = await get_customer_network_connectors_by_customer_code(
44 + customer_code,
45 + session,
46 + )
47 + if customer_integration_response.available_network_connectors == []:
48 + raise HTTPException(
49 + status_code=404,
50 + detail="Customer integration settings not found.",
51 + )
52 + return customer_integration_response
53 +
54 +
55 +def extract_fortinet_keys(
56 + customer_integration: CustomerNetworkConnectors,
57 +) -> Dict[str, str]:
58 + """
59 + Extracts the authentication keys for Office365 integration from the given customer integration.
60 +
61 + Args:
62 + customer_integration (CustomerIntegrations): The customer integration object.
63 +
64 + Returns:
65 + Dict[str, str]: A dictionary containing the authentication keys for Office365 integration.
66 +
67 + Raises:
68 + HTTPException: If no authentication keys are found for Office365 integration.
69 + """
70 + fortinet_keys = {}
71 + for subscription in customer_integration.network_connectors_subscriptions:
72 + if subscription.network_connectors_service.service_name == "Fortinet":
73 + for auth_key in subscription.network_connectors_keys:
74 + fortinet_keys[auth_key.auth_key_name] = auth_key.auth_value
75 + if not fortinet_keys:
76 + raise HTTPException(
77 + status_code=404,
78 + detail="No auth keys found for Fortinet integration. Please create auth keys for Fortinet network connector.",
79 + )
80 + return fortinet_keys
81 +
82 +
83 +@stack_provisioning_graylog_fortinet_router.post(
84 + "/graylog/provision/fortinet",
85 + response_model=ProvisionFortinetResponse,
86 + description="Provision Fortinet for the customer.",
87 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
88 +)
89 +async def provision_fortinet_route(
90 + provision_fortinet_request: ProvisionFortinetRequest,
91 + session: AsyncSession = Depends(get_db),
92 +) -> ProvisionFortinetResponse:
93 + """
94 + Provision Fortinet for the customer
95 + """
96 + customer_integration_response = await get_customer_integration_response(
97 + provision_fortinet_request.customer_code,
98 + session,
99 + )
100 +
101 + customer_integration = await find_customer_network_connector(
102 + provision_fortinet_request.customer_code,
103 + provision_fortinet_request.integration_name,
104 + customer_integration_response,
105 + )
106 +
107 + fortinet_keys = extract_fortinet_keys(customer_integration)
108 +
109 + if provision_fortinet_request.tcp_enabled and provision_fortinet_request.udp_enabled:
110 + raise HTTPException(
111 + status_code=400,
112 + detail="Both TCP and UDP are enabled. Please choose one of them.",
113 + )
114 + elif provision_fortinet_request.tcp_enabled:
115 + protocol_type = "TCP"
116 + elif provision_fortinet_request.udp_enabled:
117 + protocol_type = "UDP"
118 + else:
119 + raise HTTPException(
120 + status_code=400,
121 + detail="Either TCP or UDP should be enabled.",
122 + )
123 +
124 + return await provision_fortinet(
125 + customer_details=FortinetCustomerDetails(
126 + customer_code=provision_fortinet_request.customer_code,
127 + customer_name=customer_integration.customer_name,
128 + protocal_type=protocol_type,
129 + syslog_port=int(fortinet_keys["SYSLOG_PORT"]),
130 + ),
131 + keys=ProvisionFortinetKeys(**fortinet_keys),
132 + session=session,
133 + )
backend/app/stack_provisioning/graylog/routes/provision.py
+3 -90
@@ -1,11 +1,8 @@
1 from fastapi import APIRouter
2 -from fastapi import HTTPException
2 from fastapi import Security
3 from loguru import logger
4
5 from app.auth.utils import AuthHandler
7 -from app.connectors.graylog.services.content_packs import get_content_packs
8 -from app.connectors.graylog.services.management import get_system_info
6 from app.stack_provisioning.graylog.schema.provision import AvailableContentPacks
7 from app.stack_provisioning.graylog.schema.provision import (
8 AvailableContentPacksResponse,
@@ -13,96 +10,12 @@ from app.stack_provisioning.graylog.schema.provision import (
10 from app.stack_provisioning.graylog.schema.provision import ProvisionContentPackRequest
11 from app.stack_provisioning.graylog.schema.provision import ProvisionGraylogResponse
12 from app.stack_provisioning.graylog.services.provision import provision_content_pack
13 +from app.stack_provisioning.graylog.services.utils import does_content_pack_exist
14 +from app.stack_provisioning.graylog.services.utils import system_version_check
15
16 stack_provisioning_graylog_router = APIRouter()
17
18
20 -async def get_graylog_version() -> str:
21 - """
22 - Get the version of the Graylog instance.
23 -
24 - Returns:
25 - str: The version of the Graylog instance.
26 - """
27 - system_info = await get_system_info()
28 - return system_info.version
29 -
30 -
31 -async def system_version_check(compatible_version: str) -> bool:
32 - """
33 - Check if the Graylog version is compatible with the content pack.
34 -
35 - Args:
36 - compatible_version (str): The version of the Graylog instance.
37 -
38 - Returns:
39 - bool: True if the version is compatible, False if it is not.
40 - """
41 - system_version = await get_graylog_version()
42 - logger.info(f"Graylog System version: {system_version}")
43 -
44 - # Split the version strings at the '+' character and compare the parts before the '+'
45 - system_version = system_version.split("+")[0]
46 - compatible_version = compatible_version.split("+")[0]
47 -
48 - # Split these parts at the '.' character and convert them to integers
49 - system_version_parts = list(map(int, system_version.split(".")))
50 - compatible_version_parts = list(map(int, compatible_version.split(".")))
51 -
52 - if system_version_parts >= compatible_version_parts:
53 - return True
54 - else:
55 - raise HTTPException(
56 - status_code=400,
57 - detail=f"Graylog version {system_version} is not compatible with the content pack",
58 - )
59 -
60 -
61 -async def is_content_pack_available(content_pack_name: str) -> bool:
62 - """
63 - Check if the content pack is available for provisioning.
64 -
65 - Args:
66 - content_pack_name (str): The name of the content pack to check.
67 -
68 - Returns:
69 - bool: True if the content pack is available, False if it is not.
70 - """
71 - available_content_packs = [pack.name for pack in AvailableContentPacks]
72 - if content_pack_name in available_content_packs:
73 - logger.info(f"Content pack {content_pack_name} is available")
74 - return True
75 - else:
76 - logger.info(f"Content pack {content_pack_name} is not available")
77 - raise HTTPException(
78 - status_code=400,
79 - detail=f"Content pack {content_pack_name} is not available",
80 - )
81 -
82 -
83 -async def does_content_pack_exist(content_pack_name: str) -> bool:
84 - """
85 - Check if the content pack exists in the list of content packs.
86 -
87 - Args:
88 - content_pack_name (str): The name of the content pack to check.
89 -
90 - Returns:
91 - bool: True if the content pack exists, False if it does not.
92 - """
93 - content_packs = await get_content_packs()
94 - for content_pack in content_packs:
95 - logger.info(f"Checking content pack {content_pack.name}")
96 - if content_pack.name == content_pack_name:
97 - logger.info(f"Content pack {content_pack_name} exists")
98 - raise HTTPException(
99 - status_code=400,
100 - detail=f"Content pack {content_pack_name} already exists",
101 - )
102 - logger.info(f"Content pack {content_pack_name} does not exist")
103 - return False
104 -
105 -
19 @stack_provisioning_graylog_router.get(
20 "/graylog/available/content_packs",
21 response_model=AvailableContentPacksResponse,
@@ -136,7 +49,7 @@ async def provision_content_pack_route(
49 logger.info(f"Provisioning content pack {content_pack_request.content_pack_name.name}...")
50 await system_version_check(compatible_version="5.0.13+083613e")
51 await does_content_pack_exist(content_pack_name=content_pack_request.content_pack_name.name)
139 - await provision_content_pack(content_pack_request.content_pack_name.name)
52 + await provision_content_pack(content_pack_request)
53 return ProvisionGraylogResponse(
54 success=True,
55 message=f"{content_pack_request.content_pack_name.name} Content Pack provisioned successfully",
backend/app/stack_provisioning/graylog/schema/fortinet.py new
+72
@@ -0,0 +1,72 @@
1 +from typing import Any
2 +from typing import Dict
3 +from typing import Optional
4 +
5 +from pydantic import BaseModel
6 +from pydantic import Field
7 +from pydantic import root_validator
8 +
9 +
10 +class ProvisionFortinetRequest(BaseModel):
11 + customer_code: str = Field(
12 + ...,
13 + description="The customer code.",
14 + examples=["00002"],
15 + )
16 + integration_name: str = Field(
17 + "Fortinet",
18 + description="The integration name.",
19 + examples=["Fortinet"],
20 + )
21 + tcp_enabled: Optional[bool] = Field(
22 + False,
23 + description="The tcp enabled.",
24 + examples=[True],
25 + )
26 + udp_enabled: Optional[bool] = Field(
27 + False,
28 + description="The udp enabled.",
29 + examples=[True],
30 + )
31 +
32 + # ensure the `integration_name` is always set to "Fortinet"
33 + @root_validator(pre=True)
34 + def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
35 + values["integration_name"] = "Fortinet"
36 + return values
37 +
38 +
39 +class ProvisionFortinetResponse(BaseModel):
40 + success: bool
41 + message: str
42 +
43 +
44 +class ProvisionFortinetKeys(BaseModel):
45 + SYSLOG_PORT: str = Field(
46 + ...,
47 + description="The syslog port.",
48 + examples=["514"],
49 + )
50 +
51 +
52 +class FortinetCustomerDetails(BaseModel):
53 + customer_name: str = Field(
54 + ...,
55 + description="The customer name.",
56 + examples=["Customer 1"],
57 + )
58 + customer_code: str = Field(
59 + ...,
60 + description="The customer code.",
61 + examples=["00002"],
62 + )
63 + protocal_type: str = Field(
64 + ...,
65 + description="The protocal type.",
66 + examples=["TCP"],
67 + )
68 + syslog_port: int = Field(
69 + ...,
70 + description="The syslog port.",
71 + examples=[514],
72 + )
backend/app/stack_provisioning/graylog/schema/provision.py
+65
@@ -1,6 +1,7 @@
1 from enum import Enum
2 from typing import Any
3 from typing import List
4 +from typing import Optional
5
6 from fastapi import HTTPException
7 from pydantic import BaseModel
@@ -12,6 +13,26 @@ class AvailableContentPacks(str, Enum):
13 "The Wazuh Content Pack which includes Input, Stream, Pipeline Rules,"
14 " Pipelines, and Lookup Tables for Wazuh logs and the SOCFortress SIEM stack."
15 )
16 + # ! COMMENTING OUT UNTIL READY ! #
17 + # SOCFORTRESS_FORTINET_INPUT_SYSLOG_TCP = "The Fortinet Input Syslog TCP content pack"
18 + # SOCFORTRESS_FORTINET_INPUT_SYSLOG_UDP = "The Fortinet Input Syslog UDP content pack"
19 + # SOCFORTRESS_FORTINET_PROCESSING_PIPELINE = "The Fortinet Processing Pipeline content pack"
20 + # SOCFORTRESS_FORTINET_STREAM = "The Fortinet Stream content pack"
21 +
22 +
23 +class ContentPackKeywords(BaseModel):
24 + customer_name: Optional[str] = Field(None, description="Name of the customer")
25 + customer_code: Optional[str] = Field(None, description="Code of the customer")
26 + protocol_type: Optional[str] = Field(
27 + None,
28 + example="TCP",
29 + description="The protocol type of the content pack",
30 + )
31 + syslog_port: Optional[int] = Field(
32 + None,
33 + example=514,
34 + description="The syslog port of the content pack",
35 + )
36
37
38 class ContentPack(BaseModel):
@@ -40,12 +61,28 @@ class AvailableContentPacksResponse(BaseModel):
61 )
62
63
64 +class ProvisionNetworkContentPackRequest(BaseModel):
65 + content_pack_name: str = Field(
66 + ...,
67 + example="FORTINET",
68 + description="The name of the content pack to provision in Graylog",
69 + )
70 + keywords: Optional[ContentPackKeywords] = Field(
71 + None,
72 + description="The keywords of the content pack to provision in Graylog",
73 + )
74 +
75 +
76 class ProvisionContentPackRequest(BaseModel):
77 content_pack_name: AvailableContentPacks = Field(
78 ...,
79 example=AvailableContentPacks.SOCFORTRESS_WAZUH_CONTENT_PACK,
80 description="The name of the content pack to provision in Graylog",
81 )
82 + keywords: Optional[ContentPackKeywords] = Field(
83 + None,
84 + description="The keywords of the content pack to provision in Graylog",
85 + )
86
87 def __init__(self, **data: Any):
88 content_pack_name = data.get("content_pack_name")
@@ -70,3 +107,31 @@ class ProvisionGraylogResponse(BaseModel):
107 example="Graylog provisioned successfully",
108 description="Message from the Graylog provisioning",
109 )
110 +
111 +
112 +class ReplaceContentPackKeywords(BaseModel):
113 + REPLACE_UUID_GLOBAL: str = Field(
114 + ...,
115 + example="12345678-1234-1234-1234-123456789012",
116 + description="The UUID of the content pack",
117 + )
118 + REPLACE_UUID_SPECIFIC: str = Field(
119 + ...,
120 + example="12345678-1234-1234-1234-123456789012",
121 + description="The UUID of the input",
122 + )
123 + customer_name: str = Field(
124 + ...,
125 + example="SOCFortress",
126 + description="The name of the customer",
127 + )
128 + customer_code: str = Field(
129 + ...,
130 + example="00001",
131 + description="The code of the customer",
132 + )
133 + SYSLOG_PORT: int = Field(
134 + ...,
135 + example=514,
136 + description="The syslog port",
137 + )
backend/app/stack_provisioning/graylog/services/fortinet.py new
+25
@@ -0,0 +1,25 @@
1 +from sqlalchemy.ext.asyncio import AsyncSession
2 +
3 +from app.stack_provisioning.graylog.schema.fortinet import FortinetCustomerDetails
4 +from app.stack_provisioning.graylog.schema.fortinet import ProvisionFortinetKeys
5 +from app.stack_provisioning.graylog.schema.provision import ContentPackKeywords
6 +from app.stack_provisioning.graylog.schema.provision import (
7 + ProvisionNetworkContentPackRequest,
8 +)
9 +from app.stack_provisioning.graylog.services.provision import (
10 + provision_content_pack_network_connector,
11 +)
12 +
13 +
14 +async def provision_fortinet(customer_details: FortinetCustomerDetails, keys: ProvisionFortinetKeys, session: AsyncSession):
15 + await provision_content_pack_network_connector(
16 + content_pack_request=ProvisionNetworkContentPackRequest(
17 + content_pack_name="FORTINET",
18 + keywords=ContentPackKeywords(
19 + customer_name=customer_details.customer_name,
20 + customer_code=customer_details.customer_code,
21 + protocol_type=customer_details.protocal_type,
22 + syslog_port=customer_details.syslog_port,
23 + ),
24 + ),
25 + )
backend/app/stack_provisioning/graylog/services/provision.py
+136 -5
@@ -1,12 +1,20 @@
1 import json
2 from pathlib import Path
3 +from uuid import uuid4
4
5 from fastapi import HTTPException
6 from loguru import logger
7
8 from app.connectors.graylog.services.content_packs import insert_content_pack
9 from app.connectors.graylog.services.content_packs import install_content_pack
10 +from app.stack_provisioning.graylog.schema.provision import AvailableContentPacks
11 +from app.stack_provisioning.graylog.schema.provision import ProvisionContentPackRequest
12 from app.stack_provisioning.graylog.schema.provision import ProvisionGraylogResponse
13 +from app.stack_provisioning.graylog.schema.provision import (
14 + ProvisionNetworkContentPackRequest,
15 +)
16 +from app.stack_provisioning.graylog.schema.provision import ReplaceContentPackKeywords
17 +from app.stack_provisioning.graylog.services.utils import does_content_pack_exist
18
19
20 def get_content_pack_path(file_name: str) -> Path:
@@ -39,6 +47,7 @@ def load_content_pack_json(file_name: str) -> dict:
47 FileNotFoundError: If the dashboard JSON file is not found.
48 HTTPException: If there is an error decoding the JSON from the file.
49 """
50 + logger.info(f"Loading content pack JSON file: {file_name}")
51 file_path = get_content_pack_path(file_name)
52 try:
53 with open(file_path, "r") as file:
@@ -68,19 +77,141 @@ async def write_content_pack_to_file(content_pack: dict) -> None:
77 json.dump(content_pack, file, indent=4)
78
79
71 -async def provision_content_pack(content_pack_name: str) -> ProvisionGraylogResponse:
80 +async def retrieve_valid_content_packs(content_pack_type: str) -> list:
81 + """
82 + Returns a list of content pack template names based on the type.
83 +
84 + Args:
85 + content_pack_type (str): The type of content pack to retrieve.
86 +
87 + Returns:
88 + list: A list of content pack template names.
89 + """
90 + available_content_packs = [pack.name for pack in AvailableContentPacks]
91 + logger.info(f"Available content packs: {available_content_packs}")
92 + # Create a list of valid content pack names based on the content pack type
93 + valid_content_pack_names = []
94 + for content_pack in available_content_packs:
95 + if content_pack_type in content_pack:
96 + valid_content_pack_names.append(content_pack)
97 + return valid_content_pack_names
98 +
99 +
100 +def replace_keywords_in_json_complex(data, replacements):
101 + """
102 + Recursively replace specified keywords in JSON data, including within strings, with the provided values in the replacements dictionary.
103 +
104 + Args:
105 + data (dict or list): The JSON data in which replacements need to be made.
106 + replacements (dict): A dictionary mapping keywords to their respective replacement values.
107 +
108 + Returns:
109 + dict or list: The modified JSON data with the keywords replaced.
110 + """
111 + if isinstance(data, dict):
112 + return {key: replace_keywords_in_json_complex(value, replacements) for key, value in data.items()}
113 + elif isinstance(data, list):
114 + return [replace_keywords_in_json_complex(item, replacements) for item in data]
115 + elif isinstance(data, str):
116 + for key, value in replacements.items():
117 + data = data.replace(key, str(value))
118 + return data
119 + else:
120 + return data
121 +
122 +
123 +def convert_port_value_to_int(data):
124 + """
125 + Recursively navigates through a JSON-like dictionary and converts the port value to an integer.
126 +
127 + Args:
128 + data (dict or list): The JSON data in which the port value needs to be converted.
129 +
130 + Returns:
131 + dict or list: The modified JSON data with the port value converted to integer.
132 + """
133 + if isinstance(data, dict):
134 + for key, value in data.items():
135 + if key == "port" and isinstance(value, dict) and "@value" in value and isinstance(value["@value"], str):
136 + try:
137 + # Convert the string to an integer
138 + value["@value"] = int(value["@value"])
139 + except ValueError:
140 + # Handle the case where the string cannot be converted to an integer
141 + pass
142 + else:
143 + # Recurse into the value
144 + data[key] = convert_port_value_to_int(value)
145 + elif isinstance(data, list):
146 + # Process each item in the list
147 + data = [convert_port_value_to_int(item) for item in data]
148 + return data
149 +
150 +
151 +async def provision_content_pack(content_pack_request: ProvisionContentPackRequest) -> ProvisionGraylogResponse:
152 """
153 Provision the Wazuh Content Pack in the Graylog instance
154 """
75 - logger.info(f"Provisioning {content_pack_name} Content Pack...")
76 - content_pack = load_content_pack_json(f"{content_pack_name}.json")
155 + logger.info(
156 + f"Provisioning {content_pack_request.content_pack_name.name} Content Pack with keywords {content_pack_request.keywords} ...",
157 + )
158 +
159 + content_pack = load_content_pack_json(f"{content_pack_request.content_pack_name.name}.json")
160 # ! Only for testing purposes
161 # await write_content_pack_to_file(content_pack)
162 + # return ProvisionGraylogResponse(success=True, message=f"{content_pack_request.content_pack_name.name} Content Pack provisioned successfully")
163
80 - logger.info(f"Inserting {content_pack_name} Content Pack...")
164 + logger.info(f"Inserting {content_pack_request.content_pack_name.name} Content Pack...")
165 await insert_content_pack(content_pack)
166 # ! Content Pack ID is found in the first `id` field and the revision is found in the first `rev` field
167 id, rev = await get_id_and_rev(content_pack)
168 logger.info(f"Id: {id}, Rev: {rev}")
169 await install_content_pack(content_pack_id=id, revision=rev)
86 - return ProvisionGraylogResponse(success=True, message=f"{content_pack_name} Content Pack provisioned successfully")
170 + return ProvisionGraylogResponse(
171 + success=True,
172 + message=f"{content_pack_request.content_pack_name.name} Content Pack provisioned successfully",
173 + )
174 +
175 +
176 +async def filter_content_packs(content_packs, protocol_type):
177 + if protocol_type == "TCP":
178 + return [pack for pack in content_packs if "UDP" not in pack]
179 + if protocol_type == "UDP":
180 + return [pack for pack in content_packs if "TCP" not in pack]
181 + return content_packs
182 +
183 +
184 +async def process_content_pack(content_pack, content_pack_request):
185 + if await does_content_pack_exist(content_pack):
186 + logger.info(f"Content pack {content_pack} already exists")
187 + return
188 + content_pack = load_content_pack_json(f"{content_pack}.json")
189 + replace_content_pack_keywords = ReplaceContentPackKeywords(
190 + REPLACE_UUID_GLOBAL=str(uuid4()),
191 + REPLACE_UUID_SPECIFIC=str(uuid4()),
192 + customer_name=content_pack_request.keywords.customer_name,
193 + customer_code=content_pack_request.keywords.customer_code,
194 + SYSLOG_PORT=content_pack_request.keywords.syslog_port,
195 + )
196 + if "PROCESSING_PIPELINE" not in content_pack:
197 + content_pack = replace_keywords_in_json_complex(content_pack, replace_content_pack_keywords.dict())
198 + content_pack = convert_port_value_to_int(content_pack)
199 + await insert_and_install_content_pack(content_pack)
200 +
201 +
202 +async def insert_and_install_content_pack(content_pack):
203 + logger.info(f"Inserting {content_pack} Content Pack...")
204 + await insert_content_pack(content_pack)
205 + id, rev = await get_id_and_rev(content_pack)
206 + logger.info(f"Id: {id}, Rev: {rev}")
207 + await install_content_pack(content_pack_id=id, revision=rev)
208 +
209 +
210 +async def provision_content_pack_network_connector(content_pack_request: ProvisionNetworkContentPackRequest) -> ProvisionGraylogResponse:
211 + logger.info(f"Provisioning {content_pack_request.content_pack_name} Content Pack with keywords {content_pack_request.keywords} ...")
212 + content_packs = await retrieve_valid_content_packs(content_pack_request.content_pack_name)
213 + content_packs = await filter_content_packs(content_packs, content_pack_request.keywords.protocol_type)
214 + logger.info(f"Valid content packs: {content_packs}")
215 + for content_pack in content_packs:
216 + await process_content_pack(content_pack, content_pack_request)
217 + return ProvisionGraylogResponse(success=True, message=f"{content_pack_request.content_pack_name} Content Pack provisioned successfully")
backend/app/stack_provisioning/graylog/services/utils.py new
+92
@@ -0,0 +1,92 @@
1 +from fastapi import HTTPException
2 +from loguru import logger
3 +
4 +from app.connectors.graylog.services.content_packs import get_content_packs
5 +from app.connectors.graylog.services.management import get_system_info
6 +from app.stack_provisioning.graylog.schema.provision import AvailableContentPacks
7 +
8 +
9 +async def get_graylog_version() -> str:
10 + """
11 + Get the version of the Graylog instance.
12 +
13 + Returns:
14 + str: The version of the Graylog instance.
15 + """
16 + system_info = await get_system_info()
17 + return system_info.version
18 +
19 +
20 +async def system_version_check(compatible_version: str) -> bool:
21 + """
22 + Check if the Graylog version is compatible with the content pack.
23 +
24 + Args:
25 + compatible_version (str): The version of the Graylog instance.
26 +
27 + Returns:
28 + bool: True if the version is compatible, False if it is not.
29 + """
30 + system_version = await get_graylog_version()
31 + logger.info(f"Graylog System version: {system_version}")
32 +
33 + # Split the version strings at the '+' character and compare the parts before the '+'
34 + system_version = system_version.split("+")[0]
35 + compatible_version = compatible_version.split("+")[0]
36 +
37 + # Split these parts at the '.' character and convert them to integers
38 + system_version_parts = list(map(int, system_version.split(".")))
39 + compatible_version_parts = list(map(int, compatible_version.split(".")))
40 +
41 + if system_version_parts >= compatible_version_parts:
42 + return True
43 + else:
44 + raise HTTPException(
45 + status_code=400,
46 + detail=f"Graylog version {system_version} is not compatible with the content pack",
47 + )
48 +
49 +
50 +async def is_content_pack_available(content_pack_name: str) -> bool:
51 + """
52 + Check if the content pack is available for provisioning.
53 +
54 + Args:
55 + content_pack_name (str): The name of the content pack to check.
56 +
57 + Returns:
58 + bool: True if the content pack is available, False if it is not.
59 + """
60 + available_content_packs = [pack.name for pack in AvailableContentPacks]
61 + if content_pack_name in available_content_packs:
62 + logger.info(f"Content pack {content_pack_name} is available")
63 + return True
64 + else:
65 + logger.info(f"Content pack {content_pack_name} is not available")
66 + raise HTTPException(
67 + status_code=400,
68 + detail=f"Content pack {content_pack_name} is not available",
69 + )
70 +
71 +
72 +async def does_content_pack_exist(content_pack_name: str) -> bool:
73 + """
74 + Check if the content pack exists in the list of content packs.
75 +
76 + Args:
77 + content_pack_name (str): The name of the content pack to check.
78 +
79 + Returns:
80 + bool: True if the content pack exists, False if it does not.
81 + """
82 + content_packs = await get_content_packs()
83 + for content_pack in content_packs:
84 + logger.info(f"Checking content pack {content_pack.name}")
85 + if content_pack.name == content_pack_name:
86 + logger.info(f"Content pack {content_pack_name} exists")
87 + raise HTTPException(
88 + status_code=400,
89 + detail=f"Content pack {content_pack_name} already exists",
90 + )
91 + logger.info(f"Content pack {content_pack_name} does not exist")
92 + return False
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_FORTINET_INPUT_SYSLOG_TCP.json new
+126
@@ -0,0 +1,126 @@
1 +{
2 + "v": 1,
3 + "id": "REPLACE_UUID_GLOBAL",
4 + "rev": 1,
5 + "name": "customer_name_FORTINET_INPUT_SYSLOG_TCP",
6 + "summary": "customer_name_FORTINET_INPUT_SYSLOG_TCP",
7 + "description": "",
8 + "vendor": "SOCFortress",
9 + "url": "",
10 + "parameters": [],
11 + "entities": [
12 + {
13 + "v": "1",
14 + "type": {
15 + "name": "input",
16 + "version": "1"
17 + },
18 + "id": "REPLACE_UUID_SPECIFIC",
19 + "data": {
20 + "title": {
21 + "@type": "string",
22 + "@value": "customer_name - FORTINET LOGS AND EVENTS"
23 + },
24 + "configuration": {
25 + "tls_key_file": {
26 + "@type": "string",
27 + "@value": "soc-syslog-manager"
28 + },
29 + "port": {
30 + "@type": "integer",
31 + "@value": "SYSLOG_PORT"
32 + },
33 + "tls_enable": {
34 + "@type": "boolean",
35 + "@value": false
36 + },
37 + "use_null_delimiter": {
38 + "@type": "boolean",
39 + "@value": false
40 + },
41 + "recv_buffer_size": {
42 + "@type": "integer",
43 + "@value": 1048576
44 + },
45 + "tcp_keepalive": {
46 + "@type": "boolean",
47 + "@value": false
48 + },
49 + "force_rdns": {
50 + "@type": "boolean",
51 + "@value": false
52 + },
53 + "allow_override_date": {
54 + "@type": "boolean",
55 + "@value": false
56 + },
57 + "tls_client_auth_cert_file": {
58 + "@type": "string",
59 + "@value": ""
60 + },
61 + "bind_address": {
62 + "@type": "string",
63 + "@value": "0.0.0.0"
64 + },
65 + "tls_cert_file": {
66 + "@type": "string",
67 + "@value": ""
68 + },
69 + "expand_structured_data": {
70 + "@type": "boolean",
71 + "@value": false
72 + },
73 + "max_message_size": {
74 + "@type": "integer",
75 + "@value": 2097152
76 + },
77 + "store_full_message": {
78 + "@type": "boolean",
79 + "@value": false
80 + },
81 + "tls_client_auth": {
82 + "@type": "string",
83 + "@value": "disabled"
84 + },
85 + "charset_name": {
86 + "@type": "string",
87 + "@value": "UTF-8"
88 + },
89 + "number_worker_threads": {
90 + "@type": "integer",
91 + "@value": 4
92 + },
93 + "tls_key_password": {
94 + "@type": "string",
95 + "@value": "password"
96 + }
97 + },
98 + "static_fields": {
99 + "syslog_type": {
100 + "@type": "string",
101 + "@value": "fortinet"
102 + },
103 + "syslog_customer": {
104 + "@type": "string",
105 + "@value": "customer_code"
106 + }
107 + },
108 + "type": {
109 + "@type": "string",
110 + "@value": "org.graylog2.inputs.syslog.tcp.SyslogTCPInput"
111 + },
112 + "global": {
113 + "@type": "boolean",
114 + "@value": true
115 + },
116 + "extractors": []
117 + },
118 + "constraints": [
119 + {
120 + "type": "server-version",
121 + "version": ">=5.0.13+083613e"
122 + }
123 + ]
124 + }
125 + ]
126 +}
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_FORTINET_INPUT_SYSLOG_UDP.json new
+90
@@ -0,0 +1,90 @@
1 +{
2 + "v": 1,
3 + "id": "REPLACE_UUID_GLOBAL",
4 + "rev": 1,
5 + "name": "customer_name_FORTINET_INPUT_SYSLOG_UDP",
6 + "summary": "customer_name_FORTINET_INPUT_SYSLOG_UDP",
7 + "description": "",
8 + "vendor": "SOCFortress",
9 + "url": "",
10 + "parameters": [],
11 + "entities": [
12 + {
13 + "v": "1",
14 + "type": {
15 + "name": "input",
16 + "version": "1"
17 + },
18 + "id": "REPLACE_UUID_SPECIFIC",
19 + "data": {
20 + "title": {
21 + "@type": "string",
22 + "@value": "customer_name - FORTINET LOGS AND EVENTS"
23 + },
24 + "configuration": {
25 + "port": {
26 + "@type": "integer",
27 + "@value": "SYSLOG_PORT"
28 + },
29 + "recv_buffer_size": {
30 + "@type": "integer",
31 + "@value": 262144
32 + },
33 + "force_rdns": {
34 + "@type": "boolean",
35 + "@value": false
36 + },
37 + "allow_override_date": {
38 + "@type": "boolean",
39 + "@value": true
40 + },
41 + "bind_address": {
42 + "@type": "string",
43 + "@value": "0.0.0.0"
44 + },
45 + "expand_structured_data": {
46 + "@type": "boolean",
47 + "@value": false
48 + },
49 + "store_full_message": {
50 + "@type": "boolean",
51 + "@value": false
52 + },
53 + "charset_name": {
54 + "@type": "string",
55 + "@value": "UTF-8"
56 + },
57 + "number_worker_threads": {
58 + "@type": "integer",
59 + "@value": 8
60 + }
61 + },
62 + "static_fields": {
63 + "syslog_type": {
64 + "@type": "string",
65 + "@value": "fortinet"
66 + },
67 + "syslog_customer": {
68 + "@type": "string",
69 + "@value": "customer_code"
70 + }
71 + },
72 + "type": {
73 + "@type": "string",
74 + "@value": "org.graylog2.inputs.syslog.udp.SyslogUDPInput"
75 + },
76 + "global": {
77 + "@type": "boolean",
78 + "@value": true
79 + },
80 + "extractors": []
81 + },
82 + "constraints": [
83 + {
84 + "type": "server-version",
85 + "version": ">=5.0.13+083613e"
86 + }
87 + ]
88 + }
89 + ]
90 +}
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_FORTINET_PROCESSING_PIPELINE.json new
+350
@@ -0,0 +1,350 @@
1 +{
2 + "v": 1,
3 + "id": "368ca3ae-4418-4bbb-b3b0-054d68a01751",
4 + "rev": 1,
5 + "name": "SOCFORTRESS_FORTINET_PROCESSING_PIPELINE",
6 + "summary": "SOCFORTRESS_FORTINET_PROCESSING_PIPELINE",
7 + "description": "",
8 + "vendor": "SOCFortress",
9 + "url": "",
10 + "parameters": [],
11 + "entities": [
12 + {
13 + "v": "1",
14 + "type": {
15 + "name": "pipeline",
16 + "version": "1"
17 + },
18 + "id": "65608435-50d7-4652-8865-54787dfe5f0d",
19 + "data": {
20 + "title": {
21 + "@type": "string",
22 + "@value": "FORTINET PROCESSING PIPELINE"
23 + },
24 + "description": {
25 + "@type": "string",
26 + "@value": "FORTINET PROCESSING PIPELINE"
27 + },
28 + "source": {
29 + "@type": "string",
30 + "@value": "pipeline \"FORTINET PROCESSING PIPELINE\"\nstage 0 match pass\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - Alert\"\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - Critical\"\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - Debug\"\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - Emergency\"\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - Error\"\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - Informational\"\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - NOTICE\"\nrule \"FORTINET CREATE FIELD SYSLOG LEVEL - Warning\"\nstage 1 match pass\nrule \"DROP FORTINET SYSTEM WIRELESS LOGS - NOTICE\"\nrule \"DROP FORTINET TRAFFIC LOGS\"\nrule \"DROP FORTINET UTM APPCONTROL LOGS - INFORMATION\"\nend"
31 + },
32 + "connected_streams": []
33 + },
34 + "constraints": [
35 + {
36 + "type": "server-version",
37 + "version": ">=5.0.13+083613e"
38 + }
39 + ]
40 + },
41 + {
42 + "v": "1",
43 + "type": {
44 + "name": "pipeline_rule",
45 + "version": "1"
46 + },
47 + "id": "90379b8d-82e2-4ea9-905d-6ddc65fb9a64",
48 + "data": {
49 + "title": {
50 + "@type": "string",
51 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Emergency"
52 + },
53 + "description": {
54 + "@type": "string",
55 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Emergency"
56 + },
57 + "source": {
58 + "@type": "string",
59 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - Emergency\"\nwhen has_field(\"level\") AND to_long($message.level) == 0\nthen\nset_field(\"syslog_level\", \"Emergency\");\nend"
60 + }
61 + },
62 + "constraints": [
63 + {
64 + "type": "server-version",
65 + "version": ">=5.0.13+083613e"
66 + }
67 + ]
68 + },
69 + {
70 + "v": "1",
71 + "type": {
72 + "name": "pipeline_rule",
73 + "version": "1"
74 + },
75 + "id": "c53d3fc9-88bd-4c49-8543-c658867304d6",
76 + "data": {
77 + "title": {
78 + "@type": "string",
79 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Informational"
80 + },
81 + "description": {
82 + "@type": "string",
83 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Informational"
84 + },
85 + "source": {
86 + "@type": "string",
87 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - Informational\"\nwhen has_field(\"level\") AND to_long($message.level) == 6\nthen\nset_field(\"syslog_level\", \"Informational\");\nend"
88 + }
89 + },
90 + "constraints": [
91 + {
92 + "type": "server-version",
93 + "version": ">=5.0.13+083613e"
94 + }
95 + ]
96 + },
97 + {
98 + "v": "1",
99 + "type": {
100 + "name": "pipeline_rule",
101 + "version": "1"
102 + },
103 + "id": "d8041d5a-5064-43db-bbbf-5d5806e7f42d",
104 + "data": {
105 + "title": {
106 + "@type": "string",
107 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - NOTICE"
108 + },
109 + "description": {
110 + "@type": "string",
111 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - NOTICE"
112 + },
113 + "source": {
114 + "@type": "string",
115 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - NOTICE\"\nwhen has_field(\"level\") AND to_long($message.level) == 5\nthen\nset_field(\"syslog_level\", \"Notice\");\nend"
116 + }
117 + },
118 + "constraints": [
119 + {
120 + "type": "server-version",
121 + "version": ">=5.0.13+083613e"
122 + }
123 + ]
124 + },
125 + {
126 + "v": "1",
127 + "type": {
128 + "name": "pipeline_rule",
129 + "version": "1"
130 + },
131 + "id": "3a10b7c6-58e0-4445-8785-ec6f52700e74",
132 + "data": {
133 + "title": {
134 + "@type": "string",
135 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Warning"
136 + },
137 + "description": {
138 + "@type": "string",
139 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Warning"
140 + },
141 + "source": {
142 + "@type": "string",
143 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - Warning\"\nwhen has_field(\"level\") AND to_long($message.level) == 4\nthen\nset_field(\"syslog_level\", \"Warning\");\nend"
144 + }
145 + },
146 + "constraints": [
147 + {
148 + "type": "server-version",
149 + "version": ">=5.0.13+083613e"
150 + }
151 + ]
152 + },
153 + {
154 + "v": "1",
155 + "type": {
156 + "name": "pipeline_rule",
157 + "version": "1"
158 + },
159 + "id": "fe239c28-c37b-4d0a-b08b-945c988bb052",
160 + "data": {
161 + "title": {
162 + "@type": "string",
163 + "@value": "DROP FORTINET SYSTEM WIRELESS LOGS - NOTICE"
164 + },
165 + "description": {
166 + "@type": "string",
167 + "@value": "DROP FORTINET SYSTEM WIRELESS LOGS - NOTICE"
168 + },
169 + "source": {
170 + "@type": "string",
171 + "@value": "rule \"DROP FORTINET SYSTEM WIRELESS LOGS - NOTICE\"\nwhen\n $message.type == \"event\" AND $message.subtype == \"wireless\" AND $message.syslog_level == \"Notice\"\nthen\n drop_message();\nend"
172 + }
173 + },
174 + "constraints": [
175 + {
176 + "type": "server-version",
177 + "version": ">=5.0.13+083613e"
178 + }
179 + ]
180 + },
181 + {
182 + "v": "1",
183 + "type": {
184 + "name": "pipeline_rule",
185 + "version": "1"
186 + },
187 + "id": "a7b13aff-50b7-4280-b612-0a4436aaccbc",
188 + "data": {
189 + "title": {
190 + "@type": "string",
191 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Error"
192 + },
193 + "description": {
194 + "@type": "string",
195 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Error"
196 + },
197 + "source": {
198 + "@type": "string",
199 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - Error\"\nwhen has_field(\"level\") AND to_long($message.level) == 3\nthen\nset_field(\"syslog_level\", \"Error\");\nend"
200 + }
201 + },
202 + "constraints": [
203 + {
204 + "type": "server-version",
205 + "version": ">=5.0.13+083613e"
206 + }
207 + ]
208 + },
209 + {
210 + "v": "1",
211 + "type": {
212 + "name": "pipeline_rule",
213 + "version": "1"
214 + },
215 + "id": "ad4297a5-4e50-4780-9a23-cd59f560adeb",
216 + "data": {
217 + "title": {
218 + "@type": "string",
219 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Alert"
220 + },
221 + "description": {
222 + "@type": "string",
223 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Alert"
224 + },
225 + "source": {
226 + "@type": "string",
227 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - Alert\"\nwhen has_field(\"level\") AND to_long($message.level) == 1\nthen\nset_field(\"syslog_level\", \"Alert\");\nend"
228 + }
229 + },
230 + "constraints": [
231 + {
232 + "type": "server-version",
233 + "version": ">=5.0.13+083613e"
234 + }
235 + ]
236 + },
237 + {
238 + "v": "1",
239 + "type": {
240 + "name": "pipeline_rule",
241 + "version": "1"
242 + },
243 + "id": "ed3c5523-1b06-4b2f-8be2-68077545ddca",
244 + "data": {
245 + "title": {
246 + "@type": "string",
247 + "@value": "DROP FORTINET TRAFFIC LOGS"
248 + },
249 + "description": {
250 + "@type": "string",
251 + "@value": "DROP FORTINET TRAFFIC LOGS"
252 + },
253 + "source": {
254 + "@type": "string",
255 + "@value": "rule \"DROP FORTINET TRAFFIC LOGS\"\nwhen\n $message.type == \"traffic\"\nthen\n drop_message();\nend"
256 + }
257 + },
258 + "constraints": [
259 + {
260 + "type": "server-version",
261 + "version": ">=5.0.13+083613e"
262 + }
263 + ]
264 + },
265 + {
266 + "v": "1",
267 + "type": {
268 + "name": "pipeline_rule",
269 + "version": "1"
270 + },
271 + "id": "73150f91-f0cc-4d9e-9e85-670f03f2d203",
272 + "data": {
273 + "title": {
274 + "@type": "string",
275 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Debug"
276 + },
277 + "description": {
278 + "@type": "string",
279 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Debug"
280 + },
281 + "source": {
282 + "@type": "string",
283 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - Debug\"\nwhen has_field(\"level\") AND to_long($message.level) == 7\nthen\nset_field(\"syslog_level\", \"Debug\");\nend"
284 + }
285 + },
286 + "constraints": [
287 + {
288 + "type": "server-version",
289 + "version": ">=5.0.13+083613e"
290 + }
291 + ]
292 + },
293 + {
294 + "v": "1",
295 + "type": {
296 + "name": "pipeline_rule",
297 + "version": "1"
298 + },
299 + "id": "f361e1f9-c18c-43e7-bcb7-8e9ecd4e15af",
300 + "data": {
301 + "title": {
302 + "@type": "string",
303 + "@value": "DROP FORTINET UTM APPCONTROL LOGS - INFORMATION"
304 + },
305 + "description": {
306 + "@type": "string",
307 + "@value": "DROP FORTINET UTM APPCONTROL LOGS - INFORMATION"
308 + },
309 + "source": {
310 + "@type": "string",
311 + "@value": "rule \"DROP FORTINET UTM APPCONTROL LOGS - INFORMATION\"\nwhen\n $message.type == \"utm\" AND $message.subtype == \"app\" AND $message.syslog_level == \"information\"\nthen\n drop_message();\nend"
312 + }
313 + },
314 + "constraints": [
315 + {
316 + "type": "server-version",
317 + "version": ">=5.0.13+083613e"
318 + }
319 + ]
320 + },
321 + {
322 + "v": "1",
323 + "type": {
324 + "name": "pipeline_rule",
325 + "version": "1"
326 + },
327 + "id": "1642ba73-fc24-4613-bb3b-12f41a3a5e94",
328 + "data": {
329 + "title": {
330 + "@type": "string",
331 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Critical"
332 + },
333 + "description": {
334 + "@type": "string",
335 + "@value": "FORTINET CREATE FIELD SYSLOG LEVEL - Critical"
336 + },
337 + "source": {
338 + "@type": "string",
339 + "@value": "rule \"FORTINET CREATE FIELD SYSLOG LEVEL - Critical\"\nwhen has_field(\"level\") AND to_long($message.level) == 2\nthen\nset_field(\"syslog_level\", \"Critical\");\nend"
340 + }
341 + },
342 + "constraints": [
343 + {
344 + "type": "server-version",
345 + "version": ">=5.0.13+083613e"
346 + }
347 + ]
348 + }
349 + ]
350 +}
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_FORTINET_STREAM.json new
+102
@@ -0,0 +1,102 @@
1 +{
2 + "v": 1,
3 + "id": "REPLACE_UUID_GLOBAL",
4 + "rev": 1,
5 + "name": "customer_name_FORTINET_STREAM",
6 + "summary": "customer_name_FORTINET_STREAM",
7 + "description": "",
8 + "vendor": "SOCFortress",
9 + "url": "",
10 + "parameters": [],
11 + "entities": [
12 + {
13 + "v": "1",
14 + "type": {
15 + "name": "stream",
16 + "version": "1"
17 + },
18 + "id": "REPLACE_UUID_SPECIFIC",
19 + "data": {
20 + "alarm_callbacks": [],
21 + "outputs": [],
22 + "remove_matches": {
23 + "@type": "boolean",
24 + "@value": true
25 + },
26 + "title": {
27 + "@type": "string",
28 + "@value": "customer_name - FORTINET LOGS AND EVENTS"
29 + },
30 + "stream_rules": [
31 + {
32 + "type": {
33 + "@type": "string",
34 + "@value": "EXACT"
35 + },
36 + "field": {
37 + "@type": "string",
38 + "@value": "syslog_type"
39 + },
40 + "value": {
41 + "@type": "string",
42 + "@value": "fortinet"
43 + },
44 + "inverted": {
45 + "@type": "boolean",
46 + "@value": false
47 + },
48 + "description": {
49 + "@type": "string",
50 + "@value": ""
51 + }
52 + },
53 + {
54 + "type": {
55 + "@type": "string",
56 + "@value": "EXACT"
57 + },
58 + "field": {
59 + "@type": "string",
60 + "@value": "syslog_customer"
61 + },
62 + "value": {
63 + "@type": "string",
64 + "@value": "customer_code"
65 + },
66 + "inverted": {
67 + "@type": "boolean",
68 + "@value": false
69 + },
70 + "description": {
71 + "@type": "string",
72 + "@value": ""
73 + }
74 + }
75 + ],
76 + "alert_conditions": [],
77 + "matching_type": {
78 + "@type": "string",
79 + "@value": "AND"
80 + },
81 + "disabled": {
82 + "@type": "boolean",
83 + "@value": false
84 + },
85 + "description": {
86 + "@type": "string",
87 + "@value": "customer_name - FORTINET LOGS AND EVENTS"
88 + },
89 + "default_stream": {
90 + "@type": "boolean",
91 + "@value": false
92 + }
93 + },
94 + "constraints": [
95 + {
96 + "type": "server-version",
97 + "version": ">=5.0.13+083613e"
98 + }
99 + ]
100 + }
101 + ]
102 +}
backend/copilot.py
+4
@@ -15,6 +15,7 @@ from app.db.db_session import async_engine
15 from app.db.db_setup import add_connectors
16 from app.db.db_setup import apply_migrations
17 from app.db.db_setup import create_available_integrations
18 +from app.db.db_setup import create_available_network_connectors
19 from app.db.db_setup import create_copilot_user_if_not_exists
20 from app.db.db_setup import create_database_if_not_exists
21 from app.db.db_setup import create_roles
@@ -49,6 +50,7 @@ from app.routers import logs
50 from app.routers import mimecast
51 from app.routers import modules
52 from app.routers import monitoring_alert
53 +from app.routers import network_connectors
54 from app.routers import office365
55 from app.routers import sap_siem
56 from app.routers import scheduler
@@ -136,6 +138,7 @@ api_router.include_router(huntress.router)
138 api_router.include_router(license.router)
139 api_router.include_router(modules.router)
140 api_router.include_router(carbonblack.router)
141 +api_router.include_router(network_connectors.router)
142
143 # Include the APIRouter in the FastAPI app
144 app.include_router(api_router)
@@ -151,6 +154,7 @@ async def init_db():
154 await add_connectors(async_engine)
155 await create_roles(async_engine)
156 await create_available_integrations(async_engine)
157 + await create_available_network_connectors(async_engine)
158 await ensure_admin_user(async_engine)
159 await ensure_scheduler_user(async_engine)
160
frontend/package-lock.json
+158 -157
@@ -10,13 +10,13 @@
10 "dependencies": {
11 "@ajoelp/json-to-formdata": "^1.5.0",
12 "@f3ve/vue-markdown-it": "^0.2.2",
13 - "@fontsource/jetbrains-mono": "^5.0.19",
14 - "@fontsource/lexend": "^5.0.19",
15 - "@fontsource/public-sans": "^5.0.17",
13 + "@fontsource/jetbrains-mono": "^5.0.20",
14 + "@fontsource/lexend": "^5.0.20",
15 + "@fontsource/public-sans": "^5.0.18",
16 "@popperjs/core": "^2.11.8",
17 "@vueuse/components": "^10.9.0",
18 "@vueuse/core": "^10.9.0",
19 - "apexcharts": "^3.48.0",
19 + "apexcharts": "^3.49.0",
20 "bytes": "^3.1.2",
21 "colord": "^2.9.3",
22 "crypto-js": "^4.2.0",
@@ -35,10 +35,10 @@
35 "pinia-plugin-persistedstate": "^3.2.1",
36 "secure-ls": "^1.2.6",
37 "validator": "^13.11.0",
38 - "vue": "^3.4.23",
38 + "vue": "^3.4.25",
39 "vue-advanced-cropper": "^2.8.8",
40 "vue-highlight-words": "^3.0.1",
41 - "vue-i18n": "^9.13.0",
41 + "vue-i18n": "^9.13.1",
42 "vue-router": "^4.3.2",
43 "vue-sjv": "^0.0.6",
44 "vue3-apexcharts": "^1.5.2",
@@ -69,9 +69,9 @@
69 "@vue/test-utils": "^2.4.5",
70 "@vue/tsconfig": "^0.5.1",
71 "autoprefixer": "^10.4.19",
72 - "cypress": "^13.8.0",
72 + "cypress": "^13.8.1",
73 "eslint": "^8.57.0",
74 - "eslint-plugin-cypress": "^2.15.2",
74 + "eslint-plugin-cypress": "^3.0.0",
75 "eslint-plugin-vue": "^9.25.0",
76 "flourite": "^1.2.4",
77 "fs-extra": "^11.2.0",
@@ -89,12 +89,12 @@
89 "tailwindcss": "^3.4.3",
90 "taze": "^0.13.6",
91 "unplugin-vue-components": "^0.26.0",
92 - "vite": "^5.2.9",
92 + "vite": "^5.2.10",
93 "vite-bundle-analyzer": "^0.9.4",
94 "vite-bundle-visualizer": "^1.1.0",
95 "vite-svg-loader": "^5.1.0",
96 - "vitest": "^1.5.0",
97 - "vue-tsc": "^2.0.13"
96 + "vitest": "^1.5.1",
97 + "vue-tsc": "^2.0.14"
98 },
99 "engines": {
100 "node": ">=18.0.0"
@@ -671,6 +671,7 @@
671 },
672 "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
673 "version": "1.3.0",
674 + "extraneous": true,
675 "inBundle": true,
676 "license": "MIT",
677 "engines": {
@@ -1242,19 +1243,19 @@
1243 }
1244 },
1245 "node_modules/@fontsource/jetbrains-mono": {
1245 - "version": "5.0.19",
1246 - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.19.tgz",
1247 - "integrity": "sha512-SdwUuvdfuAvGWRRc4LOFRSmDrpkE+vFUpCtOIOUl1PpXdLfeU//93BZiGf7j/oFGSZJbHAurfux2uLT38/NIjw=="
1246 + "version": "5.0.20",
1247 + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.20.tgz",
1248 + "integrity": "sha512-QkrihYWqftzs+04TinulIhnFqNwO6990HR07iCUae/6daOZAMy7urUPzytrRT9M8KLTTHBeHOY0CKqOs1+o2OQ=="
1249 },
1250 "node_modules/@fontsource/lexend": {
1250 - "version": "5.0.19",
1251 - "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.19.tgz",
1252 - "integrity": "sha512-eXUgYzJ+XHrNvcXZLVAJRaOZm1hP6UFQvVG/w411ltb0LUT7TJ1jEXVfwlByylySx8AMdajyej0cjrVj+W8uIg=="
1251 + "version": "5.0.20",
1252 + "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.20.tgz",
1253 + "integrity": "sha512-up8jVBRNP7AMRb7MuRBU/Nh1qHqI1ZWIVfqO1fa4gzOwNEnXyPMfvNy4LV2zYqrS19MfTe5ic7kUKpohsVVsZg=="
1254 },
1255 "node_modules/@fontsource/public-sans": {
1255 - "version": "5.0.17",
1256 - "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.17.tgz",
1257 - "integrity": "sha512-s5qJhMelUX7faD9wOkBRaR24e9ZB9A/HolfAwMnJ9G+uMvnF7UF2hypBxtb30skeGgGw9LBpFAgk0bCd2v6TXg=="
1256 + "version": "5.0.18",
1257 + "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.18.tgz",
1258 + "integrity": "sha512-dYx/ULF7pRkjBO1ncCwIRXWWmI2oCsMsWxheHgjtNANm4+prtfq3gdGy3KOsuNcAhEKn6BQeIyg0hFqeoTpAaQ=="
1259 },
1260 "node_modules/@hapi/hoek": {
1261 "version": "9.3.0",
@@ -1343,12 +1344,12 @@
1344 }
1345 },
1346 "node_modules/@intlify/core-base": {
1346 - "version": "9.13.0",
1347 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.13.0.tgz",
1348 - "integrity": "sha512-Lx8+YTrFpom7AtdbbuJHzgmr612/bceHU92v8ZPU9HU9/rczf+TmCs95BxWPIR4K42xh4MVMLsNzLUWiXcNaLg==",
1347 + "version": "9.13.1",
1348 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.13.1.tgz",
1349 + "integrity": "sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==",
1350 "dependencies": {
1350 - "@intlify/message-compiler": "9.13.0",
1351 - "@intlify/shared": "9.13.0"
1351 + "@intlify/message-compiler": "9.13.1",
1352 + "@intlify/shared": "9.13.1"
1353 },
1354 "engines": {
1355 "node": ">= 16"
@@ -1358,11 +1359,11 @@
1359 }
1360 },
1361 "node_modules/@intlify/message-compiler": {
1361 - "version": "9.13.0",
1362 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.13.0.tgz",
1363 - "integrity": "sha512-zhESuudiDpFQhUOx/qrSMd7ZYHbmgCc0QzBc27cDUxaaAj3olbYJnsx3osiHPQyYnv/LuC+WTqoNOEBoHP6dqQ==",
1362 + "version": "9.13.1",
1363 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.13.1.tgz",
1364 + "integrity": "sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==",
1365 "dependencies": {
1365 - "@intlify/shared": "9.13.0",
1366 + "@intlify/shared": "9.13.1",
1367 "source-map-js": "^1.0.2"
1368 },
1369 "engines": {
@@ -1373,9 +1374,9 @@
1374 }
1375 },
1376 "node_modules/@intlify/shared": {
1376 - "version": "9.13.0",
1377 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.13.0.tgz",
1378 - "integrity": "sha512-fUwWcpDz9Wm4dSaz+6XmjoNXWBjZLJtT1Zf1cpLBELbCAOS8WBRscPtgOSfzm6JCqf5KgMI4g917f5TtEeez3A==",
1377 + "version": "9.13.1",
1378 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.13.1.tgz",
1379 + "integrity": "sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==",
1380 "engines": {
1381 "node": ">= 16"
1382 },
@@ -2360,13 +2361,13 @@
2361 }
2362 },
2363 "node_modules/@vitest/expect": {
2363 - "version": "1.5.0",
2364 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.0.tgz",
2365 - "integrity": "sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==",
2364 + "version": "1.5.1",
2365 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.1.tgz",
2366 + "integrity": "sha512-w3Bn+VUMqku+oWmxvPhTE86uMTbfmBl35aGaIPlwVW7Q89ZREC/icfo2HBsEZ3AAW6YR9lObfZKPEzstw9tJOQ==",
2367 "dev": true,
2368 "dependencies": {
2368 - "@vitest/spy": "1.5.0",
2369 - "@vitest/utils": "1.5.0",
2369 + "@vitest/spy": "1.5.1",
2370 + "@vitest/utils": "1.5.1",
2371 "chai": "^4.3.10"
2372 },
2373 "funding": {
@@ -2374,12 +2375,12 @@
2375 }
2376 },
2377 "node_modules/@vitest/runner": {
2377 - "version": "1.5.0",
2378 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.0.tgz",
2379 - "integrity": "sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==",
2378 + "version": "1.5.1",
2379 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.1.tgz",
2380 + "integrity": "sha512-mt372zsz0vFR7L1xF/ert4t+teD66oSuXoTyaZbl0eJgilvyzCKP1tJ21gVa8cDklkBOM3DLnkE1ljj/BskyEw==",
2381 "dev": true,
2382 "dependencies": {
2382 - "@vitest/utils": "1.5.0",
2383 + "@vitest/utils": "1.5.1",
2384 "p-limit": "^5.0.0",
2385 "pathe": "^1.1.1"
2386 },
@@ -2415,9 +2416,9 @@
2416 }
2417 },
2418 "node_modules/@vitest/snapshot": {
2418 - "version": "1.5.0",
2419 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.0.tgz",
2420 - "integrity": "sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==",
2419 + "version": "1.5.1",
2420 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.1.tgz",
2421 + "integrity": "sha512-h/1SGaZYXmjn6hULRBOlqam2z4oTlEe6WwARRzLErAPBqljAs6eX7tfdyN0K+MpipIwSZ5sZsubDWkCPAiVXZQ==",
2422 "dev": true,
2423 "dependencies": {
2424 "magic-string": "^0.30.5",
@@ -2429,9 +2430,9 @@
2430 }
2431 },
2432 "node_modules/@vitest/spy": {
2432 - "version": "1.5.0",
2433 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.0.tgz",
2434 - "integrity": "sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==",
2433 + "version": "1.5.1",
2434 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.1.tgz",
2435 + "integrity": "sha512-vsqczk6uPJjmPLy6AEtqfbFqgLYcGBe9BTY+XL8L6y8vrGOhyE23CJN9P/hPimKXnScbqiZ/r/UtUSOQ2jIDGg==",
2436 "dev": true,
2437 "dependencies": {
2438 "tinyspy": "^2.2.0"
@@ -2441,9 +2442,9 @@
2442 }
2443 },
2444 "node_modules/@vitest/utils": {
2444 - "version": "1.5.0",
2445 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.0.tgz",
2446 - "integrity": "sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==",
2445 + "version": "1.5.1",
2446 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.1.tgz",
2447 + "integrity": "sha512-92pE17bBXUxA0Y7goPcvnATMCuq4NQLOmqsG0e2BtzRi7KLwZB5jpiELi/8ybY8IQNWemKjSD5rMoO7xTdv8ug==",
2448 "dev": true,
2449 "dependencies": {
2450 "diff-sequences": "^29.6.3",
@@ -2465,30 +2466,30 @@
2466 }
2467 },
2468 "node_modules/@volar/language-core": {
2468 - "version": "2.2.0-alpha.8",
2469 - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.0-alpha.8.tgz",
2470 - "integrity": "sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ==",
2469 + "version": "2.2.0-alpha.10",
2470 + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.0-alpha.10.tgz",
2471 + "integrity": "sha512-njVJLtpu0zMvDaEk7K5q4BRpOgbyEUljU++un9TfJoJNhxG0z/hWwpwgTRImO42EKvwIxF3XUzeMk+qatAFy7Q==",
2472 "dev": true,
2473 "dependencies": {
2473 - "@volar/source-map": "2.2.0-alpha.8"
2474 + "@volar/source-map": "2.2.0-alpha.10"
2475 }
2476 },
2477 "node_modules/@volar/source-map": {
2477 - "version": "2.2.0-alpha.8",
2478 - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.0-alpha.8.tgz",
2479 - "integrity": "sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA==",
2478 + "version": "2.2.0-alpha.10",
2479 + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.0-alpha.10.tgz",
2480 + "integrity": "sha512-nrdWApVkP5cksAnDEyy1JD9rKdwOJsEq1B+seWO4vNXmZNcxQQCx4DULLBvKt7AzRUAQiAuw5aQkb9RBaSqdVA==",
2481 "dev": true,
2482 "dependencies": {
2483 "muggle-string": "^0.4.0"
2484 }
2485 },
2486 "node_modules/@volar/typescript": {
2486 - "version": "2.2.0-alpha.8",
2487 - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.0-alpha.8.tgz",
2488 - "integrity": "sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg==",
2487 + "version": "2.2.0-alpha.10",
2488 + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.0-alpha.10.tgz",
2489 + "integrity": "sha512-GCa0vTVVdA9ULUsu2Rx7jwsIuyZQPvPVT9o3NrANTbYv+523Ao1gv3glC5vzNSDPM6bUl37r94HbCj7KINQr+g==",
2490 "dev": true,
2491 "dependencies": {
2491 - "@volar/language-core": "2.2.0-alpha.8",
2492 + "@volar/language-core": "2.2.0-alpha.10",
2493 "path-browserify": "^1.0.1"
2494 }
2495 },
@@ -2566,49 +2567,49 @@
2567 }
2568 },
2569 "node_modules/@vue/compiler-core": {
2569 - "version": "3.4.23",
2570 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.23.tgz",
2571 - "integrity": "sha512-HAFmuVEwNqNdmk+w4VCQ2pkLk1Vw4XYiiyxEp3z/xvl14aLTUBw2OfVH3vBcx+FtGsynQLkkhK410Nah1N2yyQ==",
2570 + "version": "3.4.25",
2571 + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.25.tgz",
2572 + "integrity": "sha512-Y2pLLopaElgWnMNolgG8w3C5nNUVev80L7hdQ5iIKPtMJvhVpG0zhnBG/g3UajJmZdvW0fktyZTotEHD1Srhbg==",
2573 "dependencies": {
2573 - "@babel/parser": "^7.24.1",
2574 - "@vue/shared": "3.4.23",
2574 + "@babel/parser": "^7.24.4",
2575 + "@vue/shared": "3.4.25",
2576 "entities": "^4.5.0",
2577 "estree-walker": "^2.0.2",
2578 "source-map-js": "^1.2.0"
2579 }
2580 },
2581 "node_modules/@vue/compiler-dom": {
2581 - "version": "3.4.23",
2582 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.23.tgz",
2583 - "integrity": "sha512-t0b9WSTnCRrzsBGrDd1LNR5HGzYTr7LX3z6nNBG+KGvZLqrT0mY6NsMzOqlVMBKKXKVuusbbB5aOOFgTY+senw==",
2582 + "version": "3.4.25",
2583 + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.25.tgz",
2584 + "integrity": "sha512-Ugz5DusW57+HjllAugLci19NsDK+VyjGvmbB2TXaTcSlQxwL++2PETHx/+Qv6qFwNLzSt7HKepPe4DcTE3pBWg==",
2585 "dependencies": {
2585 - "@vue/compiler-core": "3.4.23",
2586 - "@vue/shared": "3.4.23"
2586 + "@vue/compiler-core": "3.4.25",
2587 + "@vue/shared": "3.4.25"
2588 }
2589 },
2590 "node_modules/@vue/compiler-sfc": {
2590 - "version": "3.4.23",
2591 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.23.tgz",
2592 - "integrity": "sha512-fSDTKTfzaRX1kNAUiaj8JB4AokikzStWgHooMhaxyjZerw624L+IAP/fvI4ZwMpwIh8f08PVzEnu4rg8/Npssw==",
2591 + "version": "3.4.25",
2592 + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.25.tgz",
2593 + "integrity": "sha512-m7rryuqzIoQpOBZ18wKyq05IwL6qEpZxFZfRxlNYuIPDqywrXQxgUwLXIvoU72gs6cRdY6wHD0WVZIFE4OEaAQ==",
2594 "dependencies": {
2594 - "@babel/parser": "^7.24.1",
2595 - "@vue/compiler-core": "3.4.23",
2596 - "@vue/compiler-dom": "3.4.23",
2597 - "@vue/compiler-ssr": "3.4.23",
2598 - "@vue/shared": "3.4.23",
2595 + "@babel/parser": "^7.24.4",
2596 + "@vue/compiler-core": "3.4.25",
2597 + "@vue/compiler-dom": "3.4.25",
2598 + "@vue/compiler-ssr": "3.4.25",
2599 + "@vue/shared": "3.4.25",
2600 "estree-walker": "^2.0.2",
2600 - "magic-string": "^0.30.8",
2601 + "magic-string": "^0.30.10",
2602 "postcss": "^8.4.38",
2603 "source-map-js": "^1.2.0"
2604 }
2605 },
2606 "node_modules/@vue/compiler-ssr": {
2606 - "version": "3.4.23",
2607 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.23.tgz",
2608 - "integrity": "sha512-hb6Uj2cYs+tfqz71Wj6h3E5t6OKvb4MVcM2Nl5i/z1nv1gjEhw+zYaNOV+Xwn+SSN/VZM0DgANw5TuJfxfezPg==",
2607 + "version": "3.4.25",
2608 + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.25.tgz",
2609 + "integrity": "sha512-H2ohvM/Pf6LelGxDBnfbbXFPyM4NE3hrw0e/EpwuSiYu8c819wx+SVGdJ65p/sFrYDd6OnSDxN1MB2mN07hRSQ==",
2610 "dependencies": {
2610 - "@vue/compiler-dom": "3.4.23",
2611 - "@vue/shared": "3.4.23"
2611 + "@vue/compiler-dom": "3.4.25",
2612 + "@vue/shared": "3.4.25"
2613 }
2614 },
2615 "node_modules/@vue/devtools-api": {
@@ -2860,12 +2861,12 @@
2861 }
2862 },
2863 "node_modules/@vue/language-core": {
2863 - "version": "2.0.13",
2864 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.13.tgz",
2865 - "integrity": "sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg==",
2864 + "version": "2.0.14",
2865 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.14.tgz",
2866 + "integrity": "sha512-3q8mHSNcGTR7sfp2X6jZdcb4yt8AjBXAfKk0qkZIh7GAJxOnoZ10h5HToZglw4ToFvAnq+xu/Z2FFbglh9Icag==",
2867 "dev": true,
2868 "dependencies": {
2868 - "@volar/language-core": "2.2.0-alpha.8",
2869 + "@volar/language-core": "2.2.0-alpha.10",
2870 "@vue/compiler-dom": "^3.4.0",
2871 "@vue/shared": "^3.4.0",
2872 "computeds": "^0.0.1",
@@ -2883,48 +2884,48 @@
2884 }
2885 },
2886 "node_modules/@vue/reactivity": {
2886 - "version": "3.4.23",
2887 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.23.tgz",
2888 - "integrity": "sha512-GlXR9PL+23fQ3IqnbSQ8OQKLodjqCyoCrmdLKZk3BP7jN6prWheAfU7a3mrltewTkoBm+N7qMEb372VHIkQRMQ==",
2887 + "version": "3.4.25",
2888 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.25.tgz",
2889 + "integrity": "sha512-mKbEtKr1iTxZkAG3vm3BtKHAOhuI4zzsVcN0epDldU/THsrvfXRKzq+lZnjczZGnTdh3ojd86/WrP+u9M51pWQ==",
2890 "dependencies": {
2890 - "@vue/shared": "3.4.23"
2891 + "@vue/shared": "3.4.25"
2892 }
2893 },
2894 "node_modules/@vue/runtime-core": {
2894 - "version": "3.4.23",
2895 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.23.tgz",
2896 - "integrity": "sha512-FeQ9MZEXoFzFkFiw9MQQ/FWs3srvrP+SjDKSeRIiQHIhtkzoj0X4rWQlRNHbGuSwLra6pMyjAttwixNMjc/xLw==",
2895 + "version": "3.4.25",
2896 + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.25.tgz",
2897 + "integrity": "sha512-3qhsTqbEh8BMH3pXf009epCI5E7bKu28fJLi9O6W+ZGt/6xgSfMuGPqa5HRbUxLoehTNp5uWvzCr60KuiRIL0Q==",
2898 "dependencies": {
2898 - "@vue/reactivity": "3.4.23",
2899 - "@vue/shared": "3.4.23"
2899 + "@vue/reactivity": "3.4.25",
2900 + "@vue/shared": "3.4.25"
2901 }
2902 },
2903 "node_modules/@vue/runtime-dom": {
2903 - "version": "3.4.23",
2904 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.23.tgz",
2905 - "integrity": "sha512-RXJFwwykZWBkMiTPSLEWU3kgVLNAfActBfWFlZd0y79FTUxexogd0PLG4HH2LfOktjRxV47Nulygh0JFXe5f9A==",
2904 + "version": "3.4.25",
2905 + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.25.tgz",
2906 + "integrity": "sha512-ode0sj77kuwXwSc+2Yhk8JMHZh1sZp9F/51wdBiz3KGaWltbKtdihlJFhQG4H6AY+A06zzeMLkq6qu8uDSsaoA==",
2907 "dependencies": {
2907 - "@vue/runtime-core": "3.4.23",
2908 - "@vue/shared": "3.4.23",
2908 + "@vue/runtime-core": "3.4.25",
2909 + "@vue/shared": "3.4.25",
2910 "csstype": "^3.1.3"
2911 }
2912 },
2913 "node_modules/@vue/server-renderer": {
2913 - "version": "3.4.23",
2914 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.23.tgz",
2915 - "integrity": "sha512-LDwGHtnIzvKFNS8dPJ1SSU5Gvm36p2ck8wCZc52fc3k/IfjKcwCyrWEf0Yag/2wTFUBXrqizfhK9c/mC367dXQ==",
2914 + "version": "3.4.25",
2915 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.25.tgz",
2916 + "integrity": "sha512-8VTwq0Zcu3K4dWV0jOwIVINESE/gha3ifYCOKEhxOj6MEl5K5y8J8clQncTcDhKF+9U765nRw4UdUEXvrGhyVQ==",
2917 "dependencies": {
2917 - "@vue/compiler-ssr": "3.4.23",
2918 - "@vue/shared": "3.4.23"
2918 + "@vue/compiler-ssr": "3.4.25",
2919 + "@vue/shared": "3.4.25"
2920 },
2921 "peerDependencies": {
2921 - "vue": "3.4.23"
2922 + "vue": "3.4.25"
2923 }
2924 },
2925 "node_modules/@vue/shared": {
2925 - "version": "3.4.23",
2926 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.23.tgz",
2927 - "integrity": "sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg=="
2926 + "version": "3.4.25",
2927 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.25.tgz",
2928 + "integrity": "sha512-k0yappJ77g2+KNrIaF0FFnzwLvUBLUYr8VOwz+/6vLsmItFp51AcxLL7Ey3iPd7BIRyWPOcqUjMnm7OkahXllA=="
2929 },
2930 "node_modules/@vue/test-utils": {
2931 "version": "2.4.5",
@@ -3231,9 +3232,9 @@
3232 }
3233 },
3234 "node_modules/apexcharts": {
3234 - "version": "3.48.0",
3235 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.48.0.tgz",
3236 - "integrity": "sha512-Lhpj1Ij6lKlrUke8gf+P+SE6uGUn+Pe1TnCJ+zqrY0YMvbqM3LMb1lY+eybbTczUyk0RmMZomlTa2NgX2EUs4Q==",
3235 + "version": "3.49.0",
3236 + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.0.tgz",
3237 + "integrity": "sha512-2T9HnbQFLCuYRPndQLmh+bEQFoz0meUbvASaGgiSKDuYhWcLBodJtIpKql2aOtMx4B/sHrWW0dm90HsW4+h2PQ==",
3238 "dependencies": {
3239 "@yr/monotone-cubic-spline": "^1.0.3",
3240 "svg.draggable.js": "^2.2.2",
@@ -4276,9 +4277,9 @@
4277 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4278 },
4279 "node_modules/cypress": {
4279 - "version": "13.8.0",
4280 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.0.tgz",
4281 - "integrity": "sha512-Qau//mtrwEGOU9cn2YjavECKyDUwBh8J2tit+y9s1wsv6C3BX+rlv6I9afmQnL8PmEEzJ6be7nppMHacFzZkTw==",
4280 + "version": "13.8.1",
4281 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.1.tgz",
4282 + "integrity": "sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==",
4283 "dev": true,
4284 "hasInstallScript": true,
4285 "dependencies": {
@@ -5214,15 +5215,15 @@
5215 }
5216 },
5217 "node_modules/eslint-plugin-cypress": {
5217 - "version": "2.15.2",
5218 - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.2.tgz",
5219 - "integrity": "sha512-CtcFEQTDKyftpI22FVGpx8bkpKyYXBlNge6zSo0pl5/qJvBAnzaD76Vu2AsP16d6mTj478Ldn2mhgrWV+Xr0vQ==",
5218 + "version": "3.0.0",
5219 + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-3.0.0.tgz",
5220 + "integrity": "sha512-ZQ0l8+fcWDYptaxLkmk2l77TAfmJqNM2SSbC6t9+P/GeMLOu2zq2jtJKsHh+qxZEzkm/5IfFgbwAU3P5AZf7+w==",
5221 "dev": true,
5222 "dependencies": {
5223 "globals": "^13.20.0"
5224 },
5225 "peerDependencies": {
5225 - "eslint": ">= 3.2.1"
5226 + "eslint": ">=7 <9"
5227 }
5228 },
5229 "node_modules/eslint-plugin-prettier": {
@@ -11220,9 +11221,9 @@
11221 }
11222 },
11223 "node_modules/vite": {
11223 - "version": "5.2.9",
11224 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz",
11225 - "integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==",
11224 + "version": "5.2.10",
11225 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz",
11226 + "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==",
11227 "dev": true,
11228 "dependencies": {
11229 "esbuild": "^0.20.1",
@@ -11303,9 +11304,9 @@
11304 }
11305 },
11306 "node_modules/vite-node": {
11306 - "version": "1.5.0",
11307 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.0.tgz",
11308 - "integrity": "sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==",
11307 + "version": "1.5.1",
11308 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.1.tgz",
11309 + "integrity": "sha512-HNpfV7BrAsjkYVNWIcPleJwvJmydJqqJRrRbpoQ/U7QDwJKyEzNa4g5aYg8MjXJyKsk29IUCcMLFRcsEvqUIsA==",
11310 "dev": true,
11311 "dependencies": {
11312 "cac": "^6.7.14",
@@ -11337,16 +11338,16 @@
11338 }
11339 },
11340 "node_modules/vitest": {
11340 - "version": "1.5.0",
11341 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.0.tgz",
11342 - "integrity": "sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==",
11341 + "version": "1.5.1",
11342 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.1.tgz",
11343 + "integrity": "sha512-3GvBMpoRnUNbZRX1L3mJCv3Ou3NAobb4dM48y8k9ZGwDofePpclTOyO+lqJFKSQpubH1V8tEcAEw/Y3mJKGJQQ==",
11344 "dev": true,
11345 "dependencies": {
11345 - "@vitest/expect": "1.5.0",
11346 - "@vitest/runner": "1.5.0",
11347 - "@vitest/snapshot": "1.5.0",
11348 - "@vitest/spy": "1.5.0",
11349 - "@vitest/utils": "1.5.0",
11346 + "@vitest/expect": "1.5.1",
11347 + "@vitest/runner": "1.5.1",
11348 + "@vitest/snapshot": "1.5.1",
11349 + "@vitest/spy": "1.5.1",
11350 + "@vitest/utils": "1.5.1",
11351 "acorn-walk": "^8.3.2",
11352 "chai": "^4.3.10",
11353 "debug": "^4.3.4",
@@ -11360,7 +11361,7 @@
11361 "tinybench": "^2.5.1",
11362 "tinypool": "^0.8.3",
11363 "vite": "^5.0.0",
11363 - "vite-node": "1.5.0",
11364 + "vite-node": "1.5.1",
11365 "why-is-node-running": "^2.2.2"
11366 },
11367 "bin": {
@@ -11375,8 +11376,8 @@
11376 "peerDependencies": {
11377 "@edge-runtime/vm": "*",
11378 "@types/node": "^18.0.0 || >=20.0.0",
11378 - "@vitest/browser": "1.5.0",
11379 - "@vitest/ui": "1.5.0",
11379 + "@vitest/browser": "1.5.1",
11380 + "@vitest/ui": "1.5.1",
11381 "happy-dom": "*",
11382 "jsdom": "*"
11383 },
@@ -11563,15 +11564,15 @@
11564 }
11565 },
11566 "node_modules/vue": {
11566 - "version": "3.4.23",
11567 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.23.tgz",
11568 - "integrity": "sha512-X1y6yyGJ28LMUBJ0k/qIeKHstGd+BlWQEOT40x3auJFTmpIhpbKLgN7EFsqalnJXq1Km5ybDEsp6BhuWKciUDg==",
11567 + "version": "3.4.25",
11568 + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.25.tgz",
11569 + "integrity": "sha512-HWyDqoBHMgav/OKiYA2ZQg+kjfMgLt/T0vg4cbIF7JbXAjDexRf5JRg+PWAfrAkSmTd2I8aPSXtooBFWHB98cg==",
11570 "dependencies": {
11570 - "@vue/compiler-dom": "3.4.23",
11571 - "@vue/compiler-sfc": "3.4.23",
11572 - "@vue/runtime-dom": "3.4.23",
11573 - "@vue/server-renderer": "3.4.23",
11574 - "@vue/shared": "3.4.23"
11571 + "@vue/compiler-dom": "3.4.25",
11572 + "@vue/compiler-sfc": "3.4.25",
11573 + "@vue/runtime-dom": "3.4.25",
11574 + "@vue/server-renderer": "3.4.25",
11575 + "@vue/shared": "3.4.25"
11576 },
11577 "peerDependencies": {
11578 "typescript": "*"
@@ -11640,12 +11641,12 @@
11641 }
11642 },
11643 "node_modules/vue-i18n": {
11643 - "version": "9.13.0",
11644 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.13.0.tgz",
11645 - "integrity": "sha512-NlZ+e8rhGSGNk/Vfh4IUvlPRjljPCRslbNYgQmYZY+sLXZgahw8fylQguZU3e8ntJDvitfe40f8p3udOiKMS0A==",
11644 + "version": "9.13.1",
11645 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.13.1.tgz",
11646 + "integrity": "sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==",
11647 "dependencies": {
11647 - "@intlify/core-base": "9.13.0",
11648 - "@intlify/shared": "9.13.0",
11648 + "@intlify/core-base": "9.13.1",
11649 + "@intlify/shared": "9.13.1",
11650 "@vue/devtools-api": "^6.5.0"
11651 },
11652 "engines": {
@@ -11691,13 +11692,13 @@
11692 }
11693 },
11694 "node_modules/vue-tsc": {
11694 - "version": "2.0.13",
11695 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.13.tgz",
11696 - "integrity": "sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ==",
11695 + "version": "2.0.14",
11696 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.14.tgz",
11697 + "integrity": "sha512-DgAO3U1cnCHOUO7yB35LENbkapeRsBZ7Ugq5hGz/QOHny0+1VQN8eSwSBjYbjLVPfvfw6EY7sNPjbuHHUhckcg==",
11698 "dev": true,
11699 "dependencies": {
11699 - "@volar/typescript": "2.2.0-alpha.8",
11700 - "@vue/language-core": "2.0.13",
11700 + "@volar/typescript": "2.2.0-alpha.10",
11701 + "@vue/language-core": "2.0.14",
11702 "semver": "^7.5.4"
11703 },
11704 "bin": {
frontend/package.json
+12 -12
@@ -35,13 +35,13 @@
35 "dependencies": {
36 "@ajoelp/json-to-formdata": "^1.5.0",
37 "@f3ve/vue-markdown-it": "^0.2.2",
38 - "@fontsource/jetbrains-mono": "^5.0.19",
39 - "@fontsource/lexend": "^5.0.19",
40 - "@fontsource/public-sans": "^5.0.17",
38 + "@fontsource/jetbrains-mono": "^5.0.20",
39 + "@fontsource/lexend": "^5.0.20",
40 + "@fontsource/public-sans": "^5.0.18",
41 "@popperjs/core": "^2.11.8",
42 "@vueuse/components": "^10.9.0",
43 "@vueuse/core": "^10.9.0",
44 - "apexcharts": "^3.48.0",
44 + "apexcharts": "^3.49.0",
45 "bytes": "^3.1.2",
46 "colord": "^2.9.3",
47 "crypto-js": "^4.2.0",
@@ -60,10 +60,10 @@
60 "pinia-plugin-persistedstate": "^3.2.1",
61 "secure-ls": "^1.2.6",
62 "validator": "^13.11.0",
63 - "vue": "^3.4.23",
63 + "vue": "^3.4.25",
64 "vue-advanced-cropper": "^2.8.8",
65 "vue-highlight-words": "^3.0.1",
66 - "vue-i18n": "^9.13.0",
66 + "vue-i18n": "^9.13.1",
67 "vue-router": "^4.3.2",
68 "vue-sjv": "^0.0.6",
69 "vue3-apexcharts": "^1.5.2",
@@ -94,9 +94,9 @@
94 "@vue/test-utils": "^2.4.5",
95 "@vue/tsconfig": "^0.5.1",
96 "autoprefixer": "^10.4.19",
97 - "cypress": "^13.8.0",
97 + "cypress": "^13.8.1",
98 "eslint": "^8.57.0",
99 - "eslint-plugin-cypress": "^2.15.2",
99 + "eslint-plugin-cypress": "^3.0.2",
100 "eslint-plugin-vue": "^9.25.0",
101 "flourite": "^1.2.4",
102 "fs-extra": "^11.2.0",
@@ -112,14 +112,14 @@
112 "start-server-and-test": "^2.0.3",
113 "tailwind-config-viewer": "^2.0.1",
114 "tailwindcss": "^3.4.3",
115 - "taze": "^0.13.6",
115 + "taze": "^0.13.7",
116 "unplugin-vue-components": "^0.26.0",
117 - "vite": "^5.2.9",
117 + "vite": "^5.2.10",
118 "vite-bundle-analyzer": "^0.9.4",
119 "vite-bundle-visualizer": "^1.1.0",
120 "vite-svg-loader": "^5.1.0",
121 - "vitest": "^1.5.0",
122 - "vue-tsc": "^2.0.13"
121 + "vitest": "^1.5.2",
122 + "vue-tsc": "^2.0.14"
123 },
124 "engines": {
125 "node": ">=18.0.0"
frontend/src/api/index.ts
+3 -1
@@ -18,6 +18,7 @@ import activeResponse from "./activeResponse"
18 import stackProvisioning from "./stackProvisioning"
19 import reporting from "./reporting"
20 import license from "./license"
21 +import scheduler from "./scheduler"
22
23 export default {
24 agents,
@@ -39,5 +40,6 @@ export default {
40 activeResponse,
41 stackProvisioning,
42 reporting,
42 - license
43 + license,
44 + scheduler
45 }
frontend/src/api/scheduler.ts new
+25
@@ -0,0 +1,25 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "./httpClient"
3 +import type { Job } from "@/types/scheduler"
4 +
5 +export interface UpdateJobPayload {
6 + /** minutes */
7 + time_interval: number
8 + extra_data: string
9 +}
10 +
11 +export default {
12 + getAllJobs() {
13 + return HttpClient.get<FlaskBaseResponse & { jobs: Job[] }>(`/scheduler`)
14 + },
15 + getNextRun(job_id: string) {
16 + return HttpClient.get<FlaskBaseResponse & { next_run_time: Date }>(`/scheduler/next_run/${job_id}`)
17 + },
18 + jobAction(job_id: string, action: "run" | "start" | "pause") {
19 + const endpoint = action === "run" ? "jobs/run" : action
20 + return HttpClient.post<FlaskBaseResponse>(`/scheduler/${endpoint}/${job_id}`)
21 + },
22 + updateJob(job_id: string, payload: UpdateJobPayload) {
23 + return HttpClient.put<FlaskBaseResponse>(`/scheduler/update/${job_id}`, {}, { params: payload })
24 + }
25 +}
frontend/src/components/activeResponse/ActiveResponseActions.vue
+2 -1
@@ -38,6 +38,7 @@ import { computed, ref } from "vue"
38 import { watch } from "vue"
39 import type { SupportedActiveResponse } from "@/types/activeResponse.d"
40 import ActiveResponseInvokeForm from "./ActiveResponseInvokeForm.vue"
41 +import type { Size } from "naive-ui/es/button/src/interface"
42
43 const emit = defineEmits<{
44 (e: "startLoading"): void
@@ -47,7 +48,7 @@ const emit = defineEmits<{
48 const { activeResponse, size, agentId } = defineProps<{
49 activeResponse: SupportedActiveResponse
50 agentId?: string | number
50 - size?: "tiny" | "small" | "medium" | "large"
51 + size?: Size
52 }>()
53
54 const InvokeIcon = "solar:playback-speed-outline"
frontend/src/components/activeResponse/ActiveResponseDetails.vue
+1 -1
@@ -1,6 +1,6 @@
1 <template>
2 <div class="active-response-details">
3 - <n-spin :show="loadingActiveResponse">
3 + <n-spin :show="loadingActiveResponse" class="min-h-48">
4 <Markdown v-if="activeResponseDetails?.markdown_content" :source="activeResponseDetails.markdown_content" />
5 <template v-else>
6 <n-empty description="No description found" class="justify-center h-48" v-if="!loadingActiveResponse" />
frontend/src/components/activeResponse/ActiveResponseWizardButton.vue
+3 -2
@@ -24,10 +24,11 @@ import { ref, watch } from "vue"
24 import { NButton, NModal } from "naive-ui"
25 import Icon from "@/components/common/Icon.vue"
26 import ActiveResponseWizard from "./ActiveResponseWizard.vue"
27 +import type { Size, Type } from "naive-ui/es/button/src/interface"
28
29 const { type, size } = defineProps<{
29 - size?: "tiny" | "small" | "medium" | "large"
30 - type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
30 + size?: Size
31 + type?: Type
32 }>()
33
34 const InvokeIcon = "solar:playback-speed-outline"
frontend/src/components/alerts/ThreatIntelButton.vue
+3 -2
@@ -22,10 +22,11 @@ import { ref, watch } from "vue"
22 import { NButton, NDrawer, NDrawerContent } from "naive-ui"
23 import ThreatIntelForm from "./ThreatIntelForm.vue"
24 import Icon from "@/components/common/Icon.vue"
25 +import type { Size, Type } from "naive-ui/es/button/src/interface"
26
27 const { type, size } = defineProps<{
27 - size?: "tiny" | "small" | "medium" | "large"
28 - type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
28 + size?: Size
29 + type?: Type
30 }>()
31
32 const ThreatIcon = "mynaui:info-waves"
frontend/src/components/auth/AuthForm.vue renamed
frontend/src/components/auth/ForgotPassword.vue renamed
frontend/src/components/auth/SignIn.vue renamed
frontend/src/components/auth/SignUp.vue renamed
frontend/src/components/auth/types.d.ts renamed
frontend/src/components/customers/integrations/CustomerIntegrationActions.vue
+2 -1
@@ -47,6 +47,7 @@ import Api from "@/api"
47 import { computed, h, ref } from "vue"
48 import { watch } from "vue"
49 import type { CustomerIntegration } from "@/types/integrations.d"
50 +import type { Size } from "naive-ui/es/button/src/interface"
51
52 const emit = defineEmits<{
53 (e: "startLoading"): void
@@ -58,7 +59,7 @@ const emit = defineEmits<{
59 const { integration, hideDeleteButton, size } = defineProps<{
60 integration: CustomerIntegration
61 hideDeleteButton?: boolean
61 - size?: "tiny" | "small" | "medium" | "large"
62 + size?: Size
63 }>()
64
65 const DeployIcon = "carbon:deploy"
frontend/src/components/scheduler/Item.vue new
+113
@@ -0,0 +1,113 @@
1 +<template>
2 + <div class="item flex flex-col gap-4 px-5 py-3">
3 + <div class="header-box flex justify-between gap-4">
4 + <div class="name">{{ job.id }}</div>
5 + <div class="time flex items-center gap-2">
6 + {{ formatDate(job.last_success, dFormats.datetimesec) }}
7 +
8 + <n-tooltip>
9 + <template #trigger>
10 + <Icon :name="TimeIcon"></Icon>
11 + </template>
12 + Last success time
13 + </n-tooltip>
14 + </div>
15 + </div>
16 + <div class="main-box flex justify-between gap-4 items-center">
17 + <div class="content">
18 + <div class="title">{{ job.name }}</div>
19 + <div class="description mt-1">{{ job.description }}</div>
20 + <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
21 + <Badge type="splitted">
22 + <template #label>Interval</template>
23 + <template #value>
24 + {{ job.time_interval }} {{ job.time_interval === 1 ? "minute" : "minutes" }}
25 + </template>
26 + </Badge>
27 + </div>
28 + </div>
29 +
30 + <div class="actions-box">
31 + <JobActions :job="job" />
32 + </div>
33 + </div>
34 + <div class="footer-box flex flex-col gap-4">
35 + <JobActions :job="job" size="small" inline />
36 + <div class="time w-full text-right">{{ formatDate(job.last_success, dFormats.datetimesec) }}</div>
37 + </div>
38 + </div>
39 +</template>
40 +
41 +<script setup lang="ts">
42 +import Icon from "@/components/common/Icon.vue"
43 +import Badge from "@/components/common/Badge.vue"
44 +import { NTooltip } from "naive-ui"
45 +import type { Job } from "@/types/scheduler"
46 +import { formatDate } from "@/utils"
47 +import { useSettingsStore } from "@/stores/settings"
48 +import JobActions from "./JobActions.vue"
49 +
50 +const { job } = defineProps<{ job: Job }>()
51 +
52 +const TimeIcon = "carbon:time"
53 +
54 +const dFormats = useSettingsStore().dateFormat
55 +</script>
56 +
57 +<style lang="scss" scoped>
58 +.item {
59 + border-radius: var(--border-radius);
60 + background-color: var(--bg-color);
61 + transition: all 0.2s var(--bezier-ease);
62 + border: var(--border-small-050);
63 +
64 + .header-box {
65 + font-size: 13px;
66 + font-family: var(--font-family-mono);
67 + word-break: break-word;
68 + color: var(--fg-secondary-color);
69 + }
70 + .main-box {
71 + .content {
72 + word-break: break-word;
73 +
74 + .description {
75 + color: var(--fg-secondary-color);
76 + font-size: 13px;
77 + }
78 + }
79 + }
80 +
81 + .footer-box {
82 + display: none;
83 + margin-top: 4px;
84 +
85 + .time {
86 + font-size: 13px;
87 + font-family: var(--font-family-mono);
88 + word-break: break-word;
89 + color: var(--fg-secondary-color);
90 + }
91 + }
92 +
93 + &:hover {
94 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
95 + }
96 +
97 + @container (max-width: 450px) {
98 + .header-box {
99 + .time {
100 + display: none;
101 + }
102 + }
103 + .main-box {
104 + .actions-box {
105 + display: none;
106 + }
107 + }
108 + .footer-box {
109 + display: flex;
110 + }
111 + }
112 +}
113 +</style>
frontend/src/components/scheduler/JobActions.vue new
+124
@@ -0,0 +1,124 @@
1 +<template>
2 + <div class="job-actions flex flex-col gap-3" :class="{ '!flex-row': inline }">
3 + <div class="flex gap-3 items-center">
4 + <n-button
5 + :size="size"
6 + :type="job.enabled ? 'warning' : 'success'"
7 + secondary
8 + @click="toggleState()"
9 + :loading="loadingAction"
10 + class="grow"
11 + >
12 + <template #icon>
13 + <Icon :name="job.enabled ? PauseIcon : StartIcon"></Icon>
14 + </template>
15 + {{ job.enabled ? "Pause" : "Start" }}
16 + </n-button>
17 +
18 + <NextTooltip :job-id="job.id" v-if="job.enabled && !inline" />
19 + </div>
20 + <div class="flex gap-3 items-center">
21 + <n-button :size="size" type="success" secondary @click="run()" :loading="loadingRun">
22 + <template #icon>
23 + <Icon :name="RunIcon"></Icon>
24 + </template>
25 + Run once
26 + </n-button>
27 + <n-button :size="size" secondary @click="showForm = true" :loading="loadingUpdate">
28 + <template #icon>
29 + <Icon :name="UpdatedIcon"></Icon>
30 + </template>
31 + </n-button>
32 +
33 + <NextTooltip :job-id="job.id" v-if="job.enabled && inline" />
34 + </div>
35 + </div>
36 +
37 + <n-modal
38 + v-model:show="showForm"
39 + display-directive="show"
40 + preset="card"
41 + :style="{ maxWidth: 'min(450px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
42 + :title="`Update ${job.name}`"
43 + :bordered="false"
44 + segmented
45 + >
46 + <JobForm @updated="update($event)" :job="job" />
47 + </n-modal>
48 +</template>
49 +
50 +<script setup lang="ts">
51 +import { ref, toRefs } from "vue"
52 +import { NButton, NModal, useMessage } from "naive-ui"
53 +import Icon from "@/components/common/Icon.vue"
54 +import JobForm from "./JobForm.vue"
55 +import NextTooltip from "./NextTooltip.vue"
56 +import type { Job } from "@/types/scheduler"
57 +import type { Size } from "naive-ui/es/button/src/interface"
58 +import Api from "@/api"
59 +import type { UpdateJobPayload } from "@/api/scheduler"
60 +
61 +const props = defineProps<{ job: Job; size?: Size; inline?: boolean }>()
62 +const { job, size, inline } = toRefs(props)
63 +
64 +const StartIcon = "material-symbols:autoplay"
65 +const PauseIcon = "carbon:pause-filled"
66 +const RunIcon = "carbon:play"
67 +const UpdatedIcon = "carbon:settings-adjust"
68 +
69 +const message = useMessage()
70 +const showForm = ref(false)
71 +const loadingRun = ref(false)
72 +const loadingAction = ref(false)
73 +const loadingUpdate = ref(false)
74 +
75 +function toggleState() {
76 + loadingAction.value = true
77 +
78 + const action = job.value.enabled ? "pause" : "start"
79 +
80 + Api.scheduler
81 + .jobAction(job.value.id, action)
82 + .then(res => {
83 + if (res.data.success) {
84 + job.value.enabled = action === "start"
85 + message.success(res.data?.message || "Job updated successfully.")
86 + } else {
87 + message.warning(res.data?.message || "An error occurred. Please try again later.")
88 + }
89 + })
90 + .catch(err => {
91 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
92 + })
93 + .finally(() => {
94 + loadingAction.value = false
95 + })
96 +}
97 +
98 +function run() {
99 + loadingRun.value = true
100 +
101 + Api.scheduler
102 + .jobAction(job.value.id, "run")
103 + .then(res => {
104 + if (res.data.success) {
105 + // TODO: check timezone with Taylor
106 + job.value.last_success = new Date()
107 + message.success(res.data?.message || "Job executed successfully.")
108 + } else {
109 + message.warning(res.data?.message || "An error occurred. Please try again later.")
110 + }
111 + })
112 + .catch(err => {
113 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
114 + })
115 + .finally(() => {
116 + loadingRun.value = false
117 + })
118 +}
119 +
120 +function update(payload: UpdateJobPayload) {
121 + showForm.value = false
122 + job.value.time_interval = payload.time_interval
123 +}
124 +</script>
frontend/src/components/scheduler/JobForm.vue new
+157
@@ -0,0 +1,157 @@
1 +<template>
2 + <n-spin :show="loading" class="job-form">
3 + <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
4 + <div class="flex flex-col gap-1">
5 + <n-form-item label="Time interval (minutes)" path="time_interval" class="grow">
6 + <n-input-number
7 + :min="1"
8 + v-model:value="form.time_interval"
9 + placeholder="Input time in minutes"
10 + clearable
11 + class="w-full"
12 + />
13 + </n-form-item>
14 +
15 + <div class="flex gap-3 justify-end items-center">
16 + <n-button @click="reset()" :disabled="loading">Reset</n-button>
17 + <n-button
18 + type="primary"
19 + :disabled="!isValid"
20 + @click="validate(() => submit())"
21 + :loading="submitting"
22 + >
23 + Submit
24 + </n-button>
25 + </div>
26 + </div>
27 + </n-form>
28 + </n-spin>
29 +</template>
30 +
31 +<script setup lang="ts">
32 +import { computed, ref, toRefs } from "vue"
33 +import Api from "@/api"
34 +import {
35 + useMessage,
36 + NForm,
37 + NFormItem,
38 + NButton,
39 + NSpin,
40 + NInputNumber,
41 + type FormValidationError,
42 + type FormInst,
43 + type FormRules,
44 + type FormItemRule,
45 + type MessageReactive
46 +} from "naive-ui"
47 +import _trim from "lodash/trim"
48 +import _get from "lodash/get"
49 +import type { UpdateJobPayload } from "@/api/scheduler"
50 +import type { Job } from "@/types/scheduler"
51 +
52 +const props = defineProps<{ job: Job }>()
53 +const { job } = toRefs(props)
54 +
55 +const emit = defineEmits<{
56 + (e: "updated", value: UpdateJobPayload): void
57 +}>()
58 +
59 +const submitting = ref(false)
60 +const loading = computed(() => submitting.value)
61 +const message = useMessage()
62 +const form = ref<UpdateJobPayload>(getClearForm())
63 +const formRef = ref<FormInst | null>(null)
64 +
65 +const rules: FormRules = {
66 + time_interval: {
67 + required: true,
68 + validator: validatorNumber("Alert Priority", "Required"),
69 + trigger: ["input", "blur"]
70 + }
71 +}
72 +
73 +const isValid = computed(() => {
74 + let valid = true
75 +
76 + for (const key in rules) {
77 + const rule = rules[key] as FormRules
78 +
79 + if (rule.required && !_trim(_get(form.value, key))) {
80 + valid = false
81 + }
82 + }
83 +
84 + return valid
85 +})
86 +
87 +function validatorNumber(fieldName: string, defaultMessage?: string) {
88 + return (rule: FormItemRule, value: string) => {
89 + if (!value) {
90 + return new Error(defaultMessage || `${fieldName} is required`)
91 + } else if (!/^\d*$/.test(value)) {
92 + return new Error(`${fieldName} should be an integer`)
93 + } else if (Number(value) < 1) {
94 + return new Error(`${fieldName} should be above 1`)
95 + }
96 + return true
97 + }
98 +}
99 +
100 +let validationMessage: MessageReactive | null = null
101 +
102 +function validate(cb?: () => void) {
103 + if (!formRef.value) return
104 +
105 + formRef.value.validate((errors?: Array<FormValidationError>) => {
106 + if (!errors) {
107 + validationMessage?.destroy()
108 + validationMessage = null
109 + if (cb) cb()
110 + } else {
111 + if (!validationMessage) {
112 + validationMessage = message.warning("You must fill in the required fields correctly.")
113 + }
114 + return false
115 + }
116 + })
117 +}
118 +
119 +function getClearForm(): UpdateJobPayload {
120 + return {
121 + time_interval: job.value.time_interval || 1,
122 + extra_data: ""
123 + }
124 +}
125 +
126 +function reset() {
127 + if (!loading.value) {
128 + resetForm()
129 + formRef.value?.restoreValidation()
130 + }
131 +}
132 +
133 +function resetForm() {
134 + form.value = getClearForm()
135 +}
136 +
137 +function submit() {
138 + submitting.value = true
139 +
140 + Api.scheduler
141 + .updateJob(job.value.id, form.value)
142 + .then(res => {
143 + if (res.data.success) {
144 + message.success(res.data?.message || `Job "${job.value.name}" updated successfully`)
145 + emit("updated", form.value)
146 + } else {
147 + message.warning(res.data?.message || "An error occurred. Please try again later.")
148 + }
149 + })
150 + .catch(err => {
151 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
152 + })
153 + .finally(() => {
154 + submitting.value = false
155 + })
156 +}
157 +</script>
frontend/src/components/scheduler/List.vue new
+58
@@ -0,0 +1,58 @@
1 +<template>
2 + <div class="scheduler-list">
3 + <n-spin :show="loading" class="min-h-48">
4 + <div class="list">
5 + <template v-if="jobs.length">
6 + <JobCard v-for="job of jobs" :key="job.id" :job="job" class="mb-2" />
7 + </template>
8 + <template v-else>
9 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
10 + </template>
11 + </div>
12 + </n-spin>
13 + </div>
14 +</template>
15 +
16 +<script setup lang="ts">
17 +import { ref, onBeforeMount, computed } from "vue"
18 +import { useMessage, NSpin, NEmpty } from "naive-ui"
19 +import Api from "@/api"
20 +import JobCard from "./Item.vue"
21 +import type { Job } from "@/types/scheduler"
22 +
23 +const message = useMessage()
24 +const loadingJobs = ref(false)
25 +const jobs = ref<Job[]>([])
26 +const loading = computed(() => loadingJobs.value)
27 +
28 +function getData() {
29 + loadingJobs.value = true
30 +
31 + Api.scheduler
32 + .getAllJobs()
33 + .then(res => {
34 + if (res.data.success) {
35 + jobs.value = res.data.jobs || []
36 + } else {
37 + message.warning(res.data?.message || "An error occurred. Please try again later.")
38 + }
39 + })
40 + .catch(err => {
41 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
42 + })
43 + .finally(() => {
44 + loadingJobs.value = false
45 + })
46 +}
47 +
48 +onBeforeMount(() => {
49 + getData()
50 +})
51 +</script>
52 +
53 +<style lang="scss" scoped>
54 +.list {
55 + container-type: inline-size;
56 + min-height: 200px;
57 +}
58 +</style>
frontend/src/components/scheduler/NextTooltip.vue new
+56
@@ -0,0 +1,56 @@
1 +<template>
2 + <n-tooltip @update:show="getNextRun()" placement="top-end">
3 + <template #trigger>
4 + <Icon :name="NextIcon"></Icon>
5 + </template>
6 + <template #header>Next run time:</template>
7 + <div>
8 + <n-spin :size="12" v-if="loadingNext" />
9 + <span v-if="!loadingNext">
10 + {{ nextRunTime ? formatDate(nextRunTime, dFormats.datetimesec) : "-" }}
11 + </span>
12 + </div>
13 + </n-tooltip>
14 +</template>
15 +
16 +<script setup lang="ts">
17 +import { ref, toRefs } from "vue"
18 +import { NTooltip, NSpin, useMessage } from "naive-ui"
19 +import Icon from "@/components/common/Icon.vue"
20 +import { formatDate } from "@/utils"
21 +import Api from "@/api"
22 +import { useSettingsStore } from "@/stores/settings"
23 +
24 +const props = defineProps<{ jobId: string }>()
25 +const { jobId } = toRefs(props)
26 +
27 +const NextIcon = "carbon:view-next"
28 +
29 +const message = useMessage()
30 +const dFormats = useSettingsStore().dateFormat
31 +const loadingNext = ref(false)
32 +const nextRunTime = ref<Date | null>(null)
33 +
34 +function getNextRun() {
35 + if (nextRunTime.value) {
36 + return
37 + }
38 + loadingNext.value = true
39 +
40 + Api.scheduler
41 + .getNextRun(jobId.value)
42 + .then(res => {
43 + if (res.data.success) {
44 + nextRunTime.value = res.data.next_run_time
45 + } else {
46 + message.warning(res.data?.message || "An error occurred. Please try again later.")
47 + }
48 + })
49 + .catch(err => {
50 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
51 + })
52 + .finally(() => {
53 + loadingNext.value = false
54 + })
55 +}
56 +</script>
frontend/src/components/soc/SocAlerts/SocAlertItemActions.vue
+2 -1
@@ -56,6 +56,7 @@ import Icon from "@/components/common/Icon.vue"
56 import Api from "@/api"
57 import { computed, ref, watch } from "vue"
58 import SocCaseItem from "../SocCases/SocCaseItem.vue"
59 +import type { Size } from "naive-ui/es/button/src/interface"
60
61 const emit = defineEmits<{
62 (e: "startLoading"): void
@@ -68,7 +69,7 @@ const emit = defineEmits<{
69 const { alertId, caseId, size } = defineProps<{
70 alertId?: string | number | null
71 caseId?: string | number | null
71 - size?: "tiny" | "small" | "medium" | "large"
72 + size?: Size
73 }>()
74
75 const DeleteIcon = "ph:trash"
frontend/src/components/soc/SocCases/SocCaseItemActions.vue
+2 -1
@@ -28,6 +28,7 @@ import Icon from "@/components/common/Icon.vue"
28 import Api from "@/api"
29 import { computed, watch, ref } from "vue"
30 import { StateName, type SocCase, type SocCaseExt } from "@/types/soc/case.d"
31 +import type { Size } from "naive-ui/es/button/src/interface"
32
33 const emit = defineEmits<{
34 (e: "closed"): void
@@ -40,7 +41,7 @@ const emit = defineEmits<{
41
42 const { caseData, size } = defineProps<{
43 caseData: SocCase | SocCaseExt | null
43 - size?: "tiny" | "small" | "medium" | "large"
44 + size?: Size
45 }>()
46
47 const DeleteIcon = "ph:trash"
frontend/src/components/stackProvisioning/StackProvisioningButton.vue
+3 -2
@@ -22,10 +22,11 @@ import { ref } from "vue"
22 import { NButton, NModal } from "naive-ui"
23 import Icon from "@/components/common/Icon.vue"
24 import StackProvisioningList from "./StackProvisioningList.vue"
25 +import type { Size, Type } from "naive-ui/es/button/src/interface"
26
27 const { type, size } = defineProps<{
27 - size?: "tiny" | "small" | "medium" | "large"
28 - type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
28 + size?: Size
29 + type?: Type
30 }>()
31
32 const PackIcon = "mdi:package-variant"
frontend/src/components/users/ChangePassword.vue
+3 -2
@@ -72,11 +72,12 @@ import Api from "@/api"
72 import { useAuthStore } from "@/stores/auth"
73 import passwordValidator from "password-validator"
74 import Icon from "@/components/common/Icon.vue"
75 +import type { Size, Type } from "naive-ui/es/button/src/interface"
76
77 const { type, size, username } = defineProps<{
78 username: string
78 - size?: "tiny" | "small" | "medium" | "large"
79 - type?: "default" | "tertiary" | "primary" | "info" | "success" | "warning" | "error"
79 + size?: Size
80 + type?: Type
81 }>()
82
83 const showFormDrawer = ref(false)
frontend/src/layouts/common/Navbar/items.tsx
+15 -15
@@ -13,10 +13,10 @@ const ArtifactsIcon = "carbon:document-multiple-01"
13 const SOCIcon = "carbon:security"
14 const HealthcheckIcon = "ph:heartbeat"
15 const CustomersIcon = "carbon:user-multiple"
16 -const LogsIcon = "carbon:cloud-logging"
16 const UsersIcon = "carbon:group-security"
17 const IntegrationsIcon = "carbon:ibm-cloud-direct-link-2-dedicated"
18 const ReportCreationIcon = "carbon:report-data"
19 +const SchedulerIcon = "material-symbols:autoplay"
20
21 /*eslint @typescript-eslint/no-unused-vars: "off"*/
22 export default function getItems(mode: "vertical" | "horizontal", collapsed: boolean): MenuMixedOption[] {
@@ -225,20 +225,6 @@ export default function getItems(mode: "vertical" | "horizontal", collapsed: boo
225 key: "Customers",
226 icon: renderIcon(CustomersIcon)
227 },
228 - {
229 - label: () =>
230 - h(
231 - RouterLink,
232 - {
233 - to: {
234 - name: "Logs"
235 - }
236 - },
237 - { default: () => "Logs" }
238 - ),
239 - key: "Logs",
240 - icon: renderIcon(LogsIcon)
241 - },
228 {
229 label: () =>
230 h(
@@ -280,6 +266,20 @@ export default function getItems(mode: "vertical" | "horizontal", collapsed: boo
266 ),
267 key: "ReportCreation",
268 icon: renderIcon(ReportCreationIcon)
269 + },
270 + {
271 + label: () =>
272 + h(
273 + RouterLink,
274 + {
275 + to: {
276 + name: "Scheduler"
277 + }
278 + },
279 + { default: () => "Scheduler" }
280 + ),
281 + key: "Scheduler",
282 + icon: renderIcon(SchedulerIcon)
283 }
284 ]
285 }
frontend/src/layouts/common/Toolbar/Avatar.vue
+6
@@ -14,6 +14,7 @@ import { useAuthStore } from "@/stores/auth"
14 const UserIcon = "ion:person-outline"
15 const LicenseIcon = "carbon:license"
16 const LogoutIcon = "ion:log-out-outline"
17 +const LogsIcon = "carbon:cloud-logging"
18 const ContactIcon = "ic:outline-alternate-email"
19
20 defineOptions({
@@ -35,6 +36,11 @@ const options = ref([
36 key: "route-License",
37 icon: renderIcon(LicenseIcon)
38 },
39 + {
40 + label: "Logs",
41 + key: "route-Logs",
42 + icon: renderIcon(LogsIcon)
43 + },
44 {
45 label: () =>
46 h(
frontend/src/router/index.ts
+10 -4
@@ -1,10 +1,10 @@
1 import { createRouter, createWebHistory } from "vue-router"
2 import Overview from "@/views/Overview.vue"
3 -import Login from "@/views/Auth/Login.vue"
3 +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/types.d"
7 +import type { FormType } from "@/components/auth/types.d"
8
9 const router = createRouter({
10 history: createWebHistory(import.meta.env.BASE_URL),
@@ -48,7 +48,7 @@ const router = createRouter({
48 path: ":id",
49 name: "Agent",
50 component: () => import("@/views/agents/Overview.vue"),
51 - meta: { title: "Agent" }
51 + meta: { title: "Agent", skipPin: true }
52 }
53 ]
54 },
@@ -156,6 +156,12 @@ const router = createRouter({
156 component: () => import("@/views/ReportCreation.vue"),
157 meta: { title: "Report Creation", auth: true, roles: UserRole.All }
158 },
159 + {
160 + path: "/scheduler",
161 + name: "Scheduler",
162 + component: () => import("@/views/Scheduler.vue"),
163 + meta: { title: "Scheduler", auth: true, roles: UserRole.All }
164 + },
165 {
166 path: "/license",
167 meta: {
@@ -199,7 +205,7 @@ const router = createRouter({
205 {
206 path: "/register",
207 name: "Register",
202 - component: () => import("@/views/Auth/Login.vue"),
208 + component: () => import("@/views/auth/Login.vue"),
209 props: { formType: "signup" as FormType },
210 meta: { title: "Register", forceLayout: Layout.Blank, checkAuth: true, skipPin: true }
211 },
frontend/src/types/scheduler.d.ts new
+9
@@ -0,0 +1,9 @@
1 +export interface Job {
2 + id: string
3 + name: string
4 + enabled: boolean
5 + /** minutes */
6 + time_interval: number
7 + last_success: Date
8 + description: string
9 +}
frontend/src/views/Scheduler.vue new
+9
@@ -0,0 +1,9 @@
1 +<template>
2 + <div class="page">
3 + <SchedulerList />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import SchedulerList from "@/components/scheduler/List.vue"
9 +</script>
frontend/src/views/auth/Login.vue renamed
+2 -2
@@ -16,12 +16,12 @@
16 </template>
17
18 <script lang="ts" setup>
19 -import AuthForm from "@/components/AuthForm/index.vue"
19 +import AuthForm from "@/components/auth/AuthForm.vue"
20 import { ref, computed, onBeforeMount, toRefs } from "vue"
21 import { useRoute } from "vue-router"
22 import { useThemeStore } from "@/stores/theme"
23 import { useAuthStore } from "@/stores/auth"
24 -import type { FormType } from "@/components/AuthForm/index.vue"
24 +import type { FormType } from "@/components/auth/types.d"
25
26 type Align = "left" | "center" | "right"
27