main
py 383 lines 16 KB
Raw
1 import asyncio
2
3 from apscheduler.events import EVENT_JOB_ERROR
4 from apscheduler.events import EVENT_JOB_MISSED
5 from apscheduler.executors.asyncio import AsyncIOExecutor
6 from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
7 from apscheduler.schedulers.asyncio import AsyncIOScheduler
8 from loguru import logger
9 from sqlalchemy.ext.asyncio import AsyncSession
10 from sqlalchemy.future import select
11
12 from app.db.db_session import async_engine
13 from app.db.db_session import sync_engine
14 from app.schedulers.models.scheduler import CreateSchedulerRequest
15 from app.schedulers.models.scheduler import JobMetadata
16 from app.schedulers.services.agent_sync import agent_sync
17 from app.schedulers.services.invoke_alert_creation import invoke_alert_creation_collect
18 from app.schedulers.services.invoke_carbonblack import (
19 invoke_carbonblack_integration_collect,
20 )
21 from app.schedulers.services.invoke_cato import invoke_cato_integration_collect
22 from app.schedulers.services.invoke_darktrace import (
23 invoke_darktrace_integration_collect,
24 )
25 from app.schedulers.services.invoke_duo import invoke_duo_integration_collect
26 from app.schedulers.services.invoke_huntress import invoke_huntress_integration_collect
27 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration
28 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp
29 from app.schedulers.services.invoke_palace_lesson_drainer import (
30 invoke_palace_lesson_drainer,
31 )
32 from app.schedulers.services.invoke_palace_lesson_sweeper import (
33 invoke_palace_lesson_sweeper,
34 )
35 from app.schedulers.services.invoke_sap_siem import (
36 invoke_sap_siem_integration_brute_force_failed_logins,
37 )
38 from app.schedulers.services.invoke_sap_siem import (
39 invoke_sap_siem_integration_brute_force_failed_logins_same_ip,
40 )
41 from app.schedulers.services.invoke_sap_siem import (
42 invoke_sap_siem_integration_collection,
43 )
44 from app.schedulers.services.invoke_sap_siem import (
45 invoke_sap_siem_integration_multiple_logins_same_ip_analysis,
46 )
47 from app.schedulers.services.invoke_sap_siem import (
48 invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location,
49 )
50 from app.schedulers.services.invoke_sap_siem import (
51 invoke_sap_siem_integration_same_user_failed_login_from_different_ip,
52 )
53 from app.schedulers.services.invoke_sap_siem import (
54 invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location,
55 )
56 from app.schedulers.services.invoke_sap_siem import (
57 invoke_sap_siem_integration_successful_login_after_multiple_failed_logins,
58 )
59 from app.schedulers.services.invoke_sap_siem import (
60 invoke_sap_siem_integration_successful_user_login_with_different_ip,
61 )
62 from app.schedulers.services.invoke_sap_siem import (
63 invoke_sap_siem_integration_suspicious_logins_analysis,
64 )
65 from app.schedulers.services.invoke_snapshot_and_restore import (
66 invoke_snapshot_schedules,
67 )
68 from app.schedulers.services.wazuh_index_resize import resize_wazuh_index_fields
69
70
71 def scheduler_listener(event):
72 if event.exception:
73 logger.error(f"Job {event.job_id} crashed: {event.exception}")
74 else:
75 logger.info(
76 f"Job {event.job_id} that was scheduled to run at {event.scheduled_run_time}, missed its run time by {event.scheduled_run_time - event.scheduled_run_time}",
77 )
78
79
80 # Global variable to hold the scheduler instance
81 scheduler_instance = None
82
83
84 async def init_scheduler():
85 global scheduler_instance
86 if scheduler_instance is not None:
87 logger.info("Returning existing scheduler instance.")
88 return scheduler_instance
89
90 logger.info("Initializing new scheduler...")
91 try:
92 jobstores = {"default": SQLAlchemyJobStore(engine=sync_engine)}
93 executors = {"default": AsyncIOExecutor()} # This executor can run asyncio coroutines
94 event_loop = asyncio.get_event_loop()
95 scheduler_instance = AsyncIOScheduler(event_loop=event_loop)
96 scheduler_instance.add_listener(scheduler_listener, EVENT_JOB_MISSED | EVENT_JOB_ERROR)
97 scheduler_instance.configure(jobstores=jobstores, executors=executors)
98 await initialize_job_metadata()
99 logger.info("Scheduling enabled jobs...")
100 await schedule_enabled_jobs(scheduler_instance)
101
102 if not scheduler_instance.running:
103 logger.info("Starting scheduler...")
104 scheduler_instance.start()
105 logger.info("Scheduler started.")
106
107 except Exception as e:
108 logger.error(f"Error initializing scheduler: {e}")
109 raise
110
111 return scheduler_instance
112
113
114 async def get_scheduler_instance():
115 """
116 Retrieves the current scheduler instance. Initializes one if it does not exist.
117 """
118 global scheduler_instance
119 if scheduler_instance is None:
120 return await init_scheduler()
121 return scheduler_instance
122
123
124 async def initialize_job_metadata():
125 """
126 Initializes job metadata from the database.
127 """
128 async with AsyncSession(async_engine) as session:
129 # Implement logic to initialize or update job metadata.
130 # Example: Check and add metadata for each known job
131 known_jobs = [
132 {
133 "job_id": "agent_sync",
134 "time_interval": 15,
135 "function": agent_sync,
136 "description": "Synchronizes agents with the Wazuh Manager and Velociraptor server.",
137 },
138 # {
139 # "job_id": "wazuh_index_fields_resize",
140 # "time_interval": 1,
141 # "function": resize_wazuh_index_fields,
142 # "description": "Resizes the Wazuh index fields.",
143 # },
144 {
145 "job_id": "resize_wazuh_index_fields",
146 "time_interval": 1,
147 "function": resize_wazuh_index_fields,
148 "description": "Resizes the Wazuh index fields.",
149 },
150 {
151 "job_id": "invoke_alert_creation_collect",
152 "time_interval": 5,
153 "function": invoke_alert_creation_collect,
154 "description": "Invokes alert creation collection.",
155 },
156 {
157 "job_id": "invoke_snapshot_schedules",
158 # Poll every 15 min so per-schedule scheduled_hour/minute gating
159 # in execute_snapshot_schedule has tight enough resolution.
160 # See SCHEDULE_MATCH_TOLERANCE_MINUTES in
161 # app/connectors/wazuh_indexer/services/snapshot_and_restore.py.
162 "time_interval": 15,
163 "function": invoke_snapshot_schedules,
164 "description": "Invokes Index snapshot schedules execution.",
165 },
166 {
167 "job_id": "invoke_palace_lesson_drainer",
168 "time_interval": 2,
169 "function": invoke_palace_lesson_drainer,
170 "description": "Drains pending AI analyst palace lessons into MemPalace via NanoClaw /palace/lesson.",
171 },
172 {
173 "job_id": "invoke_palace_lesson_sweeper",
174 "time_interval": 60,
175 "function": invoke_palace_lesson_sweeper,
176 "description": "Forgets expired one-off AI analyst palace lessons via NanoClaw /palace/forget.",
177 },
178 # ! Mirgrated SIGMA to VELO ! #
179 # {
180 # "job_id": "invoke_sigma_queries_collect",
181 # "time_interval": 5,
182 # "function": invoke_sigma_queries_collect,
183 # "description": "Invokes Sigma queries collection.",
184 # },
185 # {"job_id": "invoke_mimecast_integration", "time_interval": 5, "function": invoke_mimecast_integration}
186 ]
187 for job in known_jobs:
188 # Create a select statement for the JobMetadata table
189 stmt = select(JobMetadata).where(JobMetadata.job_id == job["job_id"])
190 result = await session.execute(stmt)
191 job_metadata = result.scalars().one_or_none()
192 if not job_metadata:
193 job_metadata = JobMetadata(
194 job_id=job["job_id"],
195 last_success=None,
196 time_interval=job["time_interval"],
197 enabled=True,
198 job_description=job["description"],
199 )
200 session.add(job_metadata)
201 else:
202 job_metadata.time_interval = job["time_interval"]
203 job_metadata.enabled = True
204 await session.commit()
205
206
207 async def disable_job(session, job_id):
208 """
209 Disables a job in the database based on the job ID.
210
211 Args:
212 session (AsyncSession): The database session.
213 job_id (str): The ID of the job to disable.
214 """
215 stmt = select(JobMetadata).where(JobMetadata.job_id == job_id)
216 result = await session.execute(stmt)
217 job_metadata = result.scalars().one_or_none()
218 logger.info(f"Job Metadata: {job_metadata}")
219 if job_metadata:
220 logger.info(f"Disabling job: {job_id}")
221 job_metadata.enabled = False
222 await session.commit()
223
224
225 async def schedule_enabled_jobs(scheduler):
226 """
227 Schedules jobs that are enabled in the database.
228 """
229 async with AsyncSession(async_engine) as session:
230 # ! First prexisiting jobs for alert monitoring prior to Graylog Alert Integration ! #
231 job_ids_to_disable = [
232 "invoke_wazuh_monitoring_alert",
233 "invoke_suricata_monitoring_alert",
234 "invoke_office365_exchange_online_alert",
235 "invoke_office365_threat_intel_alert",
236 "wazuh_index_fields_resize",
237 "invoke_huntress_integration_collection",
238 "invoke_cato_integration_collect",
239 ]
240
241 # Disable each job in the list
242 for job_id in job_ids_to_disable:
243 await disable_job(session, job_id)
244
245 stmt = select(JobMetadata).where(JobMetadata.enabled == True)
246 result = await session.execute(stmt)
247 job_metadatas = result.scalars().all()
248
249 for job_metadata in job_metadatas:
250 try:
251 job_function = get_function_by_name(job_metadata.job_id)
252 if asyncio.iscoroutinefunction(job_function):
253 # Adding coroutine functions directly
254 scheduler.add_job(
255 job_function,
256 "interval",
257 minutes=job_metadata.time_interval,
258 id=job_metadata.job_id,
259 replace_existing=True,
260 coalesce=True,
261 max_instances=1,
262 )
263 else:
264 # Regular functions go here
265 scheduler.add_job(
266 job_function,
267 "interval",
268 minutes=job_metadata.time_interval,
269 id=job_metadata.job_id,
270 replace_existing=True,
271 coalesce=True,
272 max_instances=1,
273 )
274 logger.info(f"Scheduled job: {job_metadata.job_id}")
275 except ValueError as e:
276 logger.error(f"Error scheduling job: {e}")
277
278
279 def get_function_by_name(function_name: str):
280 """
281 Returns a function object based on its name.
282 """
283 function_map = {
284 "agent_sync": agent_sync,
285 # "wazuh_index_fields_resize": resize_wazuh_index_fields,
286 "resize_wazuh_index_fields": resize_wazuh_index_fields,
287 "invoke_alert_creation_collect": invoke_alert_creation_collect,
288 "invoke_snapshot_schedules": invoke_snapshot_schedules,
289 "invoke_mimecast_integration": invoke_mimecast_integration,
290 "invoke_mimecast_integration_ttp": invoke_mimecast_integration_ttp,
291 "invoke_sap_siem_integration_collection": invoke_sap_siem_integration_collection,
292 "invoke_sap_siem_integration_suspicious_logins_analysis": invoke_sap_siem_integration_suspicious_logins_analysis,
293 "invoke_sap_siem_integration_multiple_logins_same_ip_analysis": invoke_sap_siem_integration_multiple_logins_same_ip_analysis,
294 "invoke_sap_siem_integration_successful_user_login_with_different_ip": invoke_sap_siem_integration_successful_user_login_with_different_ip,
295 "invoke_sap_siem_integration_same_user_failed_login_from_different_ip": invoke_sap_siem_integration_same_user_failed_login_from_different_ip,
296 "invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location": invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location,
297 "invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location": invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location,
298 "invoke_sap_siem_integration_brute_force_failed_logins": invoke_sap_siem_integration_brute_force_failed_logins,
299 "invoke_sap_siem_integration_brute_force_failed_logins_same_ip": invoke_sap_siem_integration_brute_force_failed_logins_same_ip,
300 "invoke_sap_siem_integration_successful_login_after_multiple_failed_logins": invoke_sap_siem_integration_successful_login_after_multiple_failed_logins,
301 "invoke_huntress_integration_collect": invoke_huntress_integration_collect,
302 "invoke_cato_integration_collect": invoke_cato_integration_collect,
303 "invoke_duo_integration_collect": invoke_duo_integration_collect,
304 "invoke_darktrace_integration_collect": invoke_darktrace_integration_collect,
305 "invoke_carbonblack_integration_collection": invoke_carbonblack_integration_collect,
306 "invoke_palace_lesson_drainer": invoke_palace_lesson_drainer,
307 "invoke_palace_lesson_sweeper": invoke_palace_lesson_sweeper,
308 # Add other function mappings here
309 }
310 return function_map.get(
311 function_name,
312 lambda: ValueError(f"Function {function_name} not found"),
313 )
314
315
316 async def add_scheduler_jobs(create_scheduler_request: CreateSchedulerRequest):
317 """
318 Adds a job to the scheduler.
319
320 Args:
321 create_scheduler_request (CreateSchedulerRequest): The request object containing the job details.
322 """
323 scheduler = await get_scheduler_instance()
324 logger.info(f"create_scheduler_request: {create_scheduler_request}")
325
326 job_function = get_function_by_name(create_scheduler_request.function_name)
327
328 # Here, we use the async add_job if the job function is a coroutine
329 if asyncio.iscoroutinefunction(job_function):
330 # Adding coroutine functions directly
331 scheduler.add_job(
332 job_function,
333 "interval",
334 minutes=create_scheduler_request.time_interval,
335 id=create_scheduler_request.job_id,
336 replace_existing=True,
337 coalesce=True,
338 max_instances=1,
339 )
340 else:
341 # Regular functions go here
342 scheduler.add_job(
343 job_function,
344 "interval",
345 minutes=create_scheduler_request.time_interval,
346 id=create_scheduler_request.job_id,
347 replace_existing=True,
348 coalesce=True,
349 max_instances=1,
350 )
351
352 await add_job_metadata(create_scheduler_request)
353
354 if not scheduler.running:
355 scheduler.start()
356
357
358 async def add_job_metadata(create_scheduler_request: CreateSchedulerRequest):
359 """
360 Adds a job to the scheduler.
361
362 Args:
363 create_scheduler_request (CreateSchedulerRequest): The request object containing the job details.
364 """
365 async with AsyncSession(async_engine) as session:
366 # Using SQLAlchemy 1.4+ style with select() and scalars() for fetching results
367 stmt = select(JobMetadata).where(JobMetadata.job_id == create_scheduler_request.job_id)
368 result = await session.execute(stmt)
369 job_metadata = result.scalars().one_or_none()
370
371 if not job_metadata:
372 job_metadata = JobMetadata(
373 job_id=create_scheduler_request.job_id,
374 last_success=None,
375 time_interval=create_scheduler_request.time_interval,
376 enabled=True,
377 )
378 session.add(job_metadata)
379 else:
380 job_metadata.time_interval = create_scheduler_request.time_interval
381 job_metadata.enabled = True
382
383 await session.commit()