@cryptotaxi247 / CoPilot / commits / 43ee0e8f

New agent page (#95)

* updated agents page * added AgentToolbar component * convert agents delete api endpoint to DELETE * search bar text * added cortex logo * graylog typescript interfaces * precommit fixes * add graylog to api index * add graylog input marquee * precommit fixes * change to check * Add InputState * css fix * remove stopped * updated agent toolbar * css fix * updated agents page * updated indices components for design consistency * fixed input marquee loop * details for inputs template * added agent overview page * broke up inputs to running and configured * configured inputs table * added input id * start and stop input * stop input frontend * start input frontend * graylog input state backend * routes for inputstate * input state added * graylog event definitions * graylog pipeline rules * get graylog streams * start and stop stream * updated agent page * added vulnerability-card component * quarantine endpoint with velo * quarantine and remove quarantine * velo routes refactor * updated agent page * streams to frontend * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Sep 14, 2023 at 14:35 UTC 43ee0e8f038b1d89e579f803ed82ba1ef96f5681
46 files changed +5122 -1614
backend/app/routes/agents.py
+1 -1
@@ -88,7 +88,7 @@ def sync_agents() -> Any:
88 return jsonify(result)
89
90
91 -@bp.route("/agents/<agent_id>/delete", methods=["POST"])
91 +@bp.route("/agents/<agent_id>/delete", methods=["DELETE"])
92 def delete_agent(agent_id: str) -> Any:
93 """
94 Endpoint to delete an agent.
backend/app/routes/graylog.py
+199
@@ -2,10 +2,13 @@ from flask import Blueprint
2 from flask import jsonify
3 from loguru import logger
4
5 +from app.services.Graylog.events import EventsService
6 from app.services.Graylog.index import IndexService
7 from app.services.Graylog.inputs import InputsService
8 from app.services.Graylog.messages import MessagesService
9 from app.services.Graylog.metrics import MetricsService
10 +from app.services.Graylog.pipelines import PipelinesService
11 +from app.services.Graylog.streams import StreamsService
12
13 bp = Blueprint("graylog", __name__)
14
@@ -88,3 +91,199 @@ def get_inputs() -> dict:
91 return jsonify(
92 {"running_inputs": running_inputs, "configured_inputs": configured_inputs},
93 )
94 +
95 +
96 +@bp.route("/graylog/inputs/<input_id>/state", methods=["GET"])
97 +def get_inputstate(input_id: str) -> dict:
98 + """
99 + Endpoint to collect Graylog inputstate.
100 +
101 + Returns:
102 + dict: A JSON object containing the list of all running and configured inputs.
103 + """
104 + logger.info("Received request to get graylog inputstate")
105 + service = InputsService()
106 + inputstate = service.collect_inputstate(input_id)
107 + try:
108 + # If inputstate.inputstate.message starts with `No input state`, then set the state to STOPPED
109 + if inputstate["inputstate"]["message"].startswith("No input state"):
110 + inputstate["inputstate"]["state"] = "STOPPED"
111 + return inputstate
112 + except Exception:
113 + return inputstate
114 +
115 +
116 +@bp.route("/graylog/inputs/running", methods=["GET"])
117 +def get_inputs_running() -> dict:
118 + """
119 + Endpoint to collect running Graylog inputs.
120 +
121 + Returns:
122 + dict: A JSON object containing the list of all running and configured inputs.
123 + """
124 + logger.info("Received request to get runnning graylog inputs")
125 + service = InputsService()
126 + running_inputs = service.collect_running_inputs()
127 + return jsonify(
128 + {"running_inputs": running_inputs},
129 + )
130 +
131 +
132 +@bp.route("/graylog/inputs/configured", methods=["GET"])
133 +def get_inputs_configured() -> dict:
134 + """
135 + Endpoint to collect configured Graylog inputs.
136 +
137 + Returns:
138 + dict: A JSON object containing the list of all running and configured inputs.
139 + """
140 + logger.info("Received request to get configured graylog inputs")
141 + service = InputsService()
142 + configured_inputs = service.collect_configured_inputs()
143 + for input in configured_inputs["configured_inputs"]:
144 + # Get the ID and invoke the get_inputstate function
145 + input_id = input["id"]
146 + inputstate = get_inputstate(input_id)
147 + # Add the inputstate to the configured_inputs
148 + input["inputstate"] = inputstate["inputstate"]["state"]
149 + return jsonify(
150 + {"configured_inputs": configured_inputs},
151 + )
152 +
153 +
154 +@bp.route("/graylog/inputs/<input_id>/stop", methods=["DELETE"])
155 +def stop_input(input_id: str) -> dict:
156 + """
157 + Endpoint to stop a Graylog input.
158 +
159 + Args:
160 + input_id (str): The ID of the input to be stopped.
161 +
162 + Returns:
163 + dict: A JSON object containing the result of the stop operation.
164 + """
165 + logger.info("Received request to stop input")
166 + service = InputsService()
167 + result = service.stop_input(input_id)
168 + return result
169 +
170 +
171 +@bp.route("/graylog/inputs/<input_id>/start", methods=["PUT"])
172 +def start_input(input_id: str) -> dict:
173 + """
174 + Endpoint to start a Graylog input.
175 +
176 + Args:
177 + input_id (str): The ID of the input to be started.
178 +
179 + Returns:
180 + dict: A JSON object containing the result of the start operation.
181 + """
182 + logger.info("Received request to start input")
183 + service = InputsService()
184 + result = service.start_input(input_id)
185 + return result
186 +
187 +
188 +@bp.route("/graylog/event/definitions", methods=["GET"])
189 +def get_event_definitions() -> dict:
190 + """
191 + Endpoint to collect Graylog event definitions.
192 +
193 + Returns:
194 + dict: A JSON object containing the list of all event definitions.
195 + """
196 + logger.info("Received request to get graylog event definitions")
197 + service = EventsService()
198 + event_definitions = service.collect_event_definitions()
199 + return event_definitions
200 +
201 +
202 +@bp.route("/graylog/event/alerts", methods=["GET"])
203 +def get_alerts() -> dict:
204 + """
205 + Endpoint to collect Graylog alerts. Currently collects last 100 alerts.
206 +
207 + Returns:
208 + dict: A JSON object containing the list of all alerts.
209 + """
210 + logger.info("Received request to get graylog alerts")
211 + service = EventsService()
212 + alerts = service.collect_alerts()
213 + return alerts
214 +
215 +
216 +@bp.route("/graylog/pipeline/rules", methods=["GET"])
217 +def get_pipeline_rules() -> dict:
218 + """
219 + Endpoint to collect Graylog pipeline rules.
220 +
221 + Returns:
222 + dict: A JSON object containing the list of all pipeline rules.
223 + """
224 + logger.info("Received request to get graylog pipeline rules")
225 + service = PipelinesService()
226 + pipeline_rules = service.collect_pipeline_rules()
227 + return pipeline_rules
228 +
229 +
230 +@bp.route("/graylog/pipeline/pipelines", methods=["GET"])
231 +def get_pipelines() -> dict:
232 + """
233 + Endpoint to collect Graylog pipelines.
234 +
235 + Returns:
236 + dict: A JSON object containing the list of all pipelines.
237 + """
238 + logger.info("Received request to get graylog pipelines")
239 + service = PipelinesService()
240 + pipelines = service.collect_pipelines()
241 + return pipelines
242 +
243 +
244 +@bp.route("/graylog/streams", methods=["GET"])
245 +def get_streams() -> dict:
246 + """
247 + Endpoint to collect Graylog streams.
248 +
249 + Returns:
250 + dict: A JSON object containing the list of all streams.
251 + """
252 + logger.info("Received request to get graylog streams")
253 + service = StreamsService()
254 + streams = service.collect_streams()
255 + return streams
256 +
257 +
258 +@bp.route("/graylog/streams/<stream_id>/pause", methods=["POST"])
259 +def pause_stream(stream_id: str) -> dict:
260 + """
261 + Endpoint to pause a Graylog stream.
262 +
263 + Args:
264 + stream_id (str): The ID of the stream to be paused.
265 +
266 + Returns:
267 + dict: A JSON object containing the result of the pause operation.
268 + """
269 + logger.info("Received request to pause stream")
270 + service = StreamsService()
271 + result = service.pause_stream(stream_id)
272 + return result
273 +
274 +
275 +@bp.route("/graylog/streams/<stream_id>/resume", methods=["POST"])
276 +def resume_stream(stream_id: str) -> dict:
277 + """
278 + Endpoint to resume a Graylog stream.
279 +
280 + Args:
281 + stream_id (str): The ID of the stream to be resumed.
282 +
283 + Returns:
284 + dict: A JSON object containing the result of the resume operation.
285 + """
286 + logger.info("Received request to resume stream")
287 + service = StreamsService()
288 + result = service.resume_stream(stream_id)
289 + return result
backend/app/routes/velociraptor.py
+66 -111
@@ -1,4 +1,8 @@
1 +from typing import Tuple
2 +from typing import Union
3 +
4 from flask import Blueprint
5 +from flask import Response
6 from flask import jsonify
7 from flask import request
8
@@ -8,149 +12,100 @@ from app.services.Velociraptor.universal import UniversalService
12 bp = Blueprint("velociraptor", __name__)
13
14
11 -@bp.route("/velociraptor/artifacts", methods=["GET"])
12 -def get_artifacts():
15 +def get_client_info(client_name: str) -> Tuple[Union[str, None], Union[str, None], Union[Response, None]]:
16 """
14 - Endpoint to list all available artifacts.
15 - It processes each artifact to verify the connection and returns the results.
16 -
17 - Returns:
18 - json: A JSON response containing the list of all available artifacts along with their connection verification
19 - status.
20 - """
21 - service = ArtifactsService()
22 - artifacts = service.collect_artifacts()
23 - return artifacts
17 + Fetch client information based on client name.
18
25 -
26 -@bp.route("/velociraptor/artifacts/linux", methods=["GET"])
27 -def get_artifacts_linux():
28 - """
29 - Endpoint to list all available Linux artifacts.
30 - It processes each artifact to verify the connection and returns the results where the name
31 - begins with `Linux`.
19 + Args:
20 + client_name (str): The name of the client.
21
22 Returns:
34 - json: A JSON response containing the list of all available Linux artifacts along with their connection verification
35 - status.
23 + tuple: Client ID, Client OS, and an error response if any.
24 """
37 - service = ArtifactsService()
38 - linux_artifacts = service.collect_artifacts_linux()
39 - return linux_artifacts
40 -
25 + service = UniversalService()
26 + client_info = service.get_client_id(client_name=client_name)["results"][0]
27 + if client_info is None:
28 + return (
29 + None,
30 + None,
31 + jsonify(
32 + {
33 + "message": f"{client_name} has not been seen in the last 30 seconds and may not be online with the Velociraptor server.",
34 + "success": False,
35 + },
36 + ),
37 + 500,
38 + )
39 + return client_info["client_id"], client_info["os_info"]["system"], None
40
42 -@bp.route("/velociraptor/artifacts/windows", methods=["GET"])
43 -def get_artifacts_windows():
44 - """
45 - Endpoint to list all available Windows artifacts.
46 - It processes each artifact to verify the connection and returns the results where the name
47 - begins with `Windows`.
41
49 - Returns:
50 - json: A JSON response containing the list of all available Windows artifacts along with their connection verification
51 - status.
42 +@bp.route("/velociraptor/artifacts/os/<filter_os>", methods=["GET"])
43 +def get_artifacts(filter_os: str = None) -> Response:
44 """
53 - service = ArtifactsService()
54 - windows_artifacts = service.collect_artifacts_windows()
55 - return windows_artifacts
56 -
45 + Fetch artifacts based on the OS filter if provided.
46
58 -@bp.route("/velociraptor/artifacts/mac", methods=["GET"])
59 -def get_artifacts_mac():
60 - """
61 - Endpoint to list all available MacOS artifacts.
62 - It processes each artifact to verify the connection and returns the results where the name
63 - begins with `MacOS`.
47 + Args:
48 + filter_os (str, optional): The OS filter for artifacts.
49
50 Returns:
66 - json: A JSON response containing the list of all available MacOS artifacts along with their connection verification
67 - status.
51 + Response: A Flask JSON response containing the artifacts.
52 """
53 service = ArtifactsService()
70 - mac_artifacts = service.collect_artifacts_macos()
71 - return mac_artifacts
54 + if filter_os:
55 + artifacts = service.collect_artifacts_filtered(filter_os)
56 + else:
57 + artifacts = service.collect_artifacts()
58 + return artifacts
59
60
74 -@bp.route("/velociraptor/artifacts/<hostname>", methods=["GET"])
75 -def get_artifacts_by_hostname(hostname):
61 +@bp.route("/velociraptor/artifacts/hostname/<hostname>", methods=["GET"])
62 +def get_artifacts_by_hostname(hostname: str) -> Response:
63 """
77 - Endpoint to list all available artifacts for a given hostname.
78 - It looks up the `os` for the provided `hostname` in the `agent_metadata` table and returns the artifacts for that OS.
64 + Fetch artifacts based on hostname.
65 +
66 + Args:
67 + hostname (str): The hostname for which to fetch artifacts.
68
69 Returns:
81 - json: A JSON response containing the list of all available artifacts along with their connection verification
82 - status.
70 + Response: A Flask JSON response containing the artifacts.
71 """
72 service = ArtifactsService()
73 artifacts = service.collect_artifacts_by_hostname(hostname=hostname)
74 return artifacts
75
76
89 -@bp.route("/velociraptor/artifacts/collection", methods=["POST"])
90 -def collect_artifact():
77 +@bp.route("/velociraptor/operation", methods=["POST"])
78 +def execute_operation() -> Response:
79 """
92 - Endpoint to collect an artifact.
93 - It collects the artifact name and client name from the request body and returns the results.
80 + Execute an operation like artifact collection, remote command execution, or quarantine.
81
82 Returns:
96 - json: A JSON response containing the result of the artifact collection operation.
83 + Response: A Flask JSON response containing the result of the operation.
84 """
85 req_data = request.get_json()
99 - artifact_name = req_data["artifact_name"]
86 client_name = req_data["client_name"]
101 - service = UniversalService()
102 - client_id = service.get_client_id(client_name=client_name)["results"][0]["client_id"]
103 - if client_id is None:
104 - return (
105 - jsonify(
106 - {
107 - "message": f"{client_name} has not been seen in the last 30 seconds and may not be online with the "
108 - "Velociraptor server.",
109 - "success": False,
110 - },
111 - ),
112 - 500,
113 - )
87 + operation = req_data["operation"]
88 + action = req_data.get("action", None)
89 + command = req_data.get("command", None)
90
115 - artifact_service = ArtifactsService()
116 - artifact_results = artifact_service.run_artifact_collection(
117 - client_id=client_id,
118 - artifact=artifact_name,
119 - )
120 - return artifact_results
91 + client_id, client_os, error_response = get_client_info(client_name)
92 + if error_response:
93 + return error_response
94
95 + service = ArtifactsService()
96
123 -@bp.route("/velociraptor/remotecommand", methods=["POST"])
124 -def run_remote_command():
125 - """
126 - Endpoint to run a remote command.
127 - It collects the command and client name from the request body and returns the results.
97 + if operation == "collect_artifact":
98 + artifact_name = req_data["artifact_name"]
99 + return service.run_artifact_collection(client_id=client_id, artifact=artifact_name)
100
129 - Returns:
130 - json: A JSON response containing the result of the PowerShell command execution.
131 - """
132 - req_data = request.get_json()
133 - command = req_data["command"]
134 - client_name = req_data["client_name"]
135 - artifact_name = req_data["artifact_name"]
136 - service = UniversalService()
137 - client_id = service.get_client_id(client_name=client_name)["results"][0]["client_id"]
138 - if client_id is None:
139 - return (
140 - jsonify(
141 - {
142 - "message": f"{client_name} has not been seen in the last 30 seconds and may not be online with the "
143 - "Velociraptor server.",
144 - "success": False,
145 - },
146 - ),
147 - 500,
148 - )
101 + elif operation == "run_command":
102 + artifact_name = req_data["artifact_name"]
103 + return service.run_remote_command(client_id=client_id, artifact=artifact_name, command=command)
104 +
105 + elif operation == "quarantine":
106 + if action is None:
107 + return jsonify({"message": "Action is required.", "success": False}), 500
108 + return service.quarantine_endpoint(client_id=client_id, client_os=client_os, action=action)
109
150 - artifact_service = ArtifactsService()
151 - artifact_results = artifact_service.run_remote_command(
152 - client_id=client_id,
153 - artifact=artifact_name,
154 - command=command,
155 - )
156 - return artifact_results
110 + else:
111 + return jsonify({"message": "Invalid operation", "success": False}), 400
backend/app/services/Graylog/events.py new
+123
@@ -0,0 +1,123 @@
1 +# from datetime import datetime
2 +from typing import Dict
3 +from typing import List
4 +from typing import Union
5 +
6 +import requests
7 +from loguru import logger
8 +
9 +from app.services.Graylog.universal import UniversalService
10 +
11 +# from typing import List
12 +
13 +
14 +class EventsService:
15 + """
16 + A service class that encapsulates the logic for pulling event data from Graylog.
17 + """
18 +
19 + HEADERS: Dict[str, str] = {"X-Requested-By": "CoPilot"}
20 +
21 + def __init__(self):
22 + """
23 + Initializes the InputsService by collecting Graylog details.
24 + """
25 + (
26 + self.connector_url,
27 + self.connector_username,
28 + self.connector_password,
29 + ) = UniversalService().collect_graylog_details("Graylog")
30 +
31 + def collect_event_definitions(
32 + self,
33 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
34 + """
35 + Collects the event definitions that are managed by Graylog.
36 +
37 + Returns:
38 + dict: A dictionary containing the success status, a message, and potentially a list of event definitions.
39 + """
40 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
41 + return {"message": "Failed to collect Graylog details", "success": False}
42 +
43 + event_definitions = self._collect_event_definitions()
44 +
45 + if event_definitions["success"]:
46 + return event_definitions
47 +
48 + def _collect_event_definitions(
49 + self,
50 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
51 + """
52 + Collects the event definitions that are managed by Graylog.
53 +
54 + Returns:
55 + dict: A dictionary containing the success status, a message, and potentially a list of event definitions.
56 + """
57 + try:
58 + event_definitions = requests.get(
59 + f"{self.connector_url}/api/events/definitions",
60 + headers=self.HEADERS,
61 + auth=(self.connector_username, self.connector_password),
62 + verify=False,
63 + )
64 + return {
65 + "message": "Successfully collected event definitions",
66 + "success": True,
67 + "event_definitions": event_definitions.json()["event_definitions"],
68 + }
69 + except Exception as e:
70 + logger.error(f"Failed to collect event definitions: {e}")
71 + return {"message": "Failed to collect event definitions", "success": False}
72 +
73 + def collect_alerts(
74 + self,
75 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
76 + """
77 + Collects the alerts that are managed by Graylog.
78 +
79 + Returns:
80 + dict: A dictionary containing the success status, a message, and potentially a list of alerts.
81 + """
82 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
83 + return {"message": "Failed to collect Graylog details", "success": False}
84 +
85 + alerts = self._collect_alerts()
86 +
87 + if alerts["success"]:
88 + return alerts
89 +
90 + def _collect_alerts(
91 + self,
92 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
93 + """
94 + Collects the alerts that are managed by Graylog.
95 +
96 + Returns:
97 + dict: A dictionary containing the success status, a message, and potentially a list of alerts.
98 + """
99 + try:
100 + # Set body as `{"query":"","page":1,"per_page":10,"filter":{"alerts":"only","event_definitions":[]},"timerange":{"range":86400,"type":"relative"}}`
101 + body = {
102 + "query": "",
103 + "page": 1,
104 + "per_page": 100,
105 + "filter": {"alerts": "only", "event_definitions": []},
106 + # "timerange": {"range": 86400, "type": "relative"},
107 + }
108 +
109 + alerts = requests.post(
110 + f"{self.connector_url}/api/events/search",
111 + headers=self.HEADERS,
112 + json=body,
113 + auth=(self.connector_username, self.connector_password),
114 + verify=False,
115 + )
116 + return {
117 + "message": "Successfully collected alerts",
118 + "success": True,
119 + "alerts": alerts.json(),
120 + }
121 + except Exception as e:
122 + logger.error(f"Failed to collect alerts: {e}")
123 + return {"message": "Failed to collect alerts", "success": False}
backend/app/services/Graylog/inputs.py
+148
@@ -65,6 +65,7 @@ class InputsService:
65 for input in running_inputs.json()["states"]:
66 inputs_list.append(
67 {
68 + "id": input["id"],
69 "state": input["state"],
70 "title": input["message_input"]["title"],
71 "port": input["message_input"]["attributes"]["port"],
@@ -116,6 +117,7 @@ class InputsService:
117 for input in configured_inputs.json()["inputs"]:
118 configured_inputs_list.append(
119 {
120 + "id": input["id"],
121 "title": input["title"],
122 "port": input["attributes"]["port"],
123 },
@@ -128,3 +130,149 @@ class InputsService:
130 except Exception as e:
131 logger.error(f"Failed to collect configured inputs: {e}")
132 return {"message": "Failed to collect configured inputs", "success": False}
133 +
134 + def collect_inputstate(
135 + self,
136 + input_id: str,
137 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
138 + """
139 + Collects the inputstate that is managed by Graylog.
140 +
141 + Returns:
142 + dict: A dictionary containing the success status, a message, and potentially a list of configured inputs.
143 + """
144 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
145 + return {"message": "Failed to collect Graylog details", "success": False}
146 +
147 + inputstate = self._collect_inputstate(input_id)
148 +
149 + if inputstate["success"]:
150 + return inputstate
151 +
152 + def _collect_inputstate(
153 + self,
154 + input_id: str,
155 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
156 + """
157 + Collects the inputstate that is managed by Graylog.
158 +
159 + Returns:
160 + dict: A dictionary containing the success status, a message, and potentially a list of configured inputs.
161 + """
162 + try:
163 + inputstate = requests.get(
164 + f"{self.connector_url}/api/system/inputstates/{input_id}",
165 + headers=self.HEADERS,
166 + auth=(self.connector_username, self.connector_password),
167 + verify=False,
168 + )
169 + return {
170 + "message": "Successfully collected inputstate",
171 + "success": True,
172 + "inputstate": inputstate.json(),
173 + }
174 + except Exception as e:
175 + logger.error(f"Failed to collect inputstate: {e}")
176 + return {"message": "Failed to collect inputstate", "success": False}
177 +
178 + def stop_input(self, input_id: str) -> Dict[str, Union[bool, str]]:
179 + """
180 + Stops a Graylog input.
181 +
182 + Args:
183 + input_id (str): The ID of the input to stop.
184 +
185 + Returns:
186 + dict: A dictionary containing the success status and a message.
187 + """
188 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
189 + return {"message": "Failed to collect Graylog details", "success": False}
190 +
191 + stop_input = self._stop_input(input_id)
192 +
193 + if stop_input["success"]:
194 + return stop_input
195 +
196 + def _stop_input(self, input_id: str) -> Dict[str, Union[bool, str]]:
197 + """
198 + Stops a Graylog input.
199 +
200 + Args:
201 + input_id (str): The ID of the input to stop.
202 +
203 + Returns:
204 + dict: A dictionary containing the success status and a message.
205 + """
206 + try:
207 + stop_input = requests.delete(
208 + f"{self.connector_url}/api/system/inputstates/{input_id}",
209 + headers=self.HEADERS,
210 + auth=(self.connector_username, self.connector_password),
211 + verify=False,
212 + )
213 + if stop_input.status_code == 200:
214 + return {
215 + "message": "Successfully stopped input",
216 + "success": True,
217 + "input_id": input_id,
218 + }
219 + else:
220 + return {
221 + "message": "Failed to stop input",
222 + "success": False,
223 + "input_id": input_id,
224 + }
225 + except Exception as e:
226 + logger.error(f"Failed to stop input: {e}")
227 + return {"message": "Failed to stop input", "success": False, "input_id": input_id}
228 +
229 + def start_input(self, input_id: str) -> Dict[str, Union[bool, str]]:
230 + """
231 + Starts a Graylog input.
232 +
233 + Args:
234 + input_id (str): The ID of the input to start.
235 +
236 + Returns:
237 + dict: A dictionary containing the success status and a message.
238 + """
239 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
240 + return {"message": "Failed to collect Graylog details", "success": False}
241 +
242 + start_input = self._start_input(input_id)
243 +
244 + if start_input["success"]:
245 + return start_input
246 +
247 + def _start_input(self, input_id: str) -> Dict[str, Union[bool, str]]:
248 + """
249 + Starts a Graylog input.
250 +
251 + Args:
252 + input_id (str): The ID of the input to start.
253 +
254 + Returns:
255 + dict: A dictionary containing the success status and a message.
256 + """
257 + try:
258 + start_input = requests.put(
259 + f"{self.connector_url}/api/system/inputstates/{input_id}",
260 + headers=self.HEADERS,
261 + auth=(self.connector_username, self.connector_password),
262 + verify=False,
263 + )
264 + if start_input.status_code == 200:
265 + return {
266 + "message": "Successfully started input",
267 + "success": True,
268 + "input_id": input_id,
269 + }
270 + else:
271 + return {
272 + "message": "Failed to start input",
273 + "success": False,
274 + "input_id": input_id,
275 + }
276 + except Exception as e:
277 + logger.error(f"Failed to start input: {e}")
278 + return {"message": "Failed to start input", "success": False, "input_id": input_id}
backend/app/services/Graylog/pipelines.py new
+113
@@ -0,0 +1,113 @@
1 +# from datetime import datetime
2 +from typing import Dict
3 +from typing import List
4 +from typing import Union
5 +
6 +import requests
7 +from loguru import logger
8 +
9 +from app.services.Graylog.universal import UniversalService
10 +
11 +# from typing import List
12 +
13 +
14 +class PipelinesService:
15 + """
16 + A service class that encapsulates the logic for pulling pipeline data from Graylog.
17 + """
18 +
19 + HEADERS: Dict[str, str] = {"X-Requested-By": "CoPilot"}
20 +
21 + def __init__(self):
22 + """
23 + Initializes the InputsService by collecting Graylog details.
24 + """
25 + (
26 + self.connector_url,
27 + self.connector_username,
28 + self.connector_password,
29 + ) = UniversalService().collect_graylog_details("Graylog")
30 +
31 + def collect_pipeline_rules(
32 + self,
33 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
34 + """
35 + Collects the pipeline rules that are managed by Graylog.
36 +
37 + Returns:
38 + dict: A dictionary containing the success status, a message, and potentially a list of pipeline rules.
39 + """
40 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
41 + return {"message": "Failed to collect Graylog details", "success": False}
42 +
43 + pipeline_rules = self._collect_pipeline_rules()
44 +
45 + if pipeline_rules["success"]:
46 + return pipeline_rules
47 +
48 + def _collect_pipeline_rules(
49 + self,
50 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
51 + """
52 + Collects the pipeline rules that are managed by Graylog.
53 +
54 + Returns:
55 + dict: A dictionary containing the success status, a message, and potentially a list of pipeline rules.
56 + """
57 + try:
58 + pipeline_rules = requests.get(
59 + f"{self.connector_url}/api/system/pipelines/rule",
60 + headers=self.HEADERS,
61 + auth=(self.connector_username, self.connector_password),
62 + verify=False,
63 + )
64 + return {
65 + "message": "Successfully collected pipeline rules",
66 + "success": True,
67 + "pipeline_rules": pipeline_rules.json(),
68 + }
69 + except Exception as e:
70 + logger.error(f"Failed to collect pipeline rules: {e}")
71 + return {"message": "Failed to collect pipeline rules", "success": False}
72 +
73 + def collect_pipelines(
74 + self,
75 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
76 + """
77 + Collects the pipelines that are managed by Graylog.
78 +
79 + Returns:
80 + dict: A dictionary containing the success status, a message, and potentially a list of pipelines.
81 + """
82 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
83 + return {"message": "Failed to collect Graylog details", "success": False}
84 +
85 + pipelines = self._collect_pipelines()
86 +
87 + if pipelines["success"]:
88 + return pipelines
89 +
90 + def _collect_pipelines(
91 + self,
92 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
93 + """
94 + Collects the pipelines that are managed by Graylog.
95 +
96 + Returns:
97 + dict: A dictionary containing the success status, a message, and potentially a list of pipelines.
98 + """
99 + try:
100 + pipelines = requests.get(
101 + f"{self.connector_url}/api/system/pipelines/pipeline",
102 + headers=self.HEADERS,
103 + auth=(self.connector_username, self.connector_password),
104 + verify=False,
105 + )
106 + return {
107 + "message": "Successfully collected pipelines",
108 + "success": True,
109 + "pipelines": pipelines.json(),
110 + }
111 + except Exception as e:
112 + logger.error(f"Failed to collect pipelines: {e}")
113 + return {"message": "Failed to collect pipelines", "success": False}
backend/app/services/Graylog/streams.py new
+157
@@ -0,0 +1,157 @@
1 +# from datetime import datetime
2 +from typing import Dict
3 +from typing import List
4 +from typing import Union
5 +
6 +import requests
7 +from loguru import logger
8 +
9 +from app.services.Graylog.universal import UniversalService
10 +
11 +# from typing import List
12 +
13 +
14 +class StreamsService:
15 + """
16 + A service class that encapsulates the logic for pulling pipeline data from Graylog.
17 + """
18 +
19 + HEADERS: Dict[str, str] = {"X-Requested-By": "CoPilot"}
20 +
21 + def __init__(self):
22 + """
23 + Initializes the InputsService by collecting Graylog details.
24 + """
25 + (
26 + self.connector_url,
27 + self.connector_username,
28 + self.connector_password,
29 + ) = UniversalService().collect_graylog_details("Graylog")
30 +
31 + def collect_streams(
32 + self,
33 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
34 + """
35 + Collects the streams that are managed by Graylog.
36 +
37 + Returns:
38 + dict: A dictionary containing the success status, a message, and potentially a list of streams.
39 + """
40 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
41 + return {"message": "Failed to collect Graylog details", "success": False}
42 +
43 + streams = self._collect_streams()
44 +
45 + if streams["success"]:
46 + return streams
47 +
48 + def _collect_streams(
49 + self,
50 + ) -> Dict[str, Union[bool, str, List[Dict[str, Union[str, int]]]]]:
51 + """
52 + Collects the streams that are managed by Graylog.
53 +
54 + Returns:
55 + dict: A dictionary containing the success status, a message, and potentially a list of streams.
56 + """
57 + try:
58 + streams = requests.get(
59 + f"{self.connector_url}/api/streams",
60 + headers=self.HEADERS,
61 + auth=(self.connector_username, self.connector_password),
62 + verify=False,
63 + )
64 + return {
65 + "message": "Successfully collected streams",
66 + "success": True,
67 + "streams": streams.json(),
68 + }
69 + except Exception as e:
70 + logger.error(f"Failed to collect streams: {e}")
71 + return {"message": "Failed to collect streams", "success": False}
72 +
73 + def pause_stream(self, stream_id: str) -> Dict[str, Union[bool, str]]:
74 + """
75 + Pauses a stream in Graylog.
76 +
77 + Args:
78 + stream_id (str): The ID of the stream to pause.
79 +
80 + Returns:
81 + dict: A dictionary containing the success status and a message.
82 + """
83 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
84 + return {"message": "Failed to collect Graylog details", "success": False}
85 +
86 + pause_stream = self._pause_stream(stream_id)
87 +
88 + if pause_stream["success"]:
89 + return pause_stream
90 +
91 + def _pause_stream(self, stream_id: str) -> Dict[str, Union[bool, str]]:
92 + """
93 + Pauses a stream in Graylog.
94 +
95 + Args:
96 + stream_id (str): The ID of the stream to pause.
97 +
98 + Returns:
99 + dict: A dictionary containing the success status and a message.
100 + """
101 + try:
102 + requests.post(
103 + f"{self.connector_url}/api/streams/{stream_id}/pause",
104 + headers=self.HEADERS,
105 + auth=(self.connector_username, self.connector_password),
106 + verify=False,
107 + )
108 + return {
109 + "message": "Successfully paused stream",
110 + "success": True,
111 + }
112 + except Exception as e:
113 + logger.error(f"Failed to pause stream: {e}")
114 + return {"message": "Failed to pause stream", "success": False}
115 +
116 + def resume_stream(self, stream_id: str) -> Dict[str, Union[bool, str]]:
117 + """
118 + Resumes a stream in Graylog.
119 +
120 + Args:
121 + stream_id (str): The ID of the stream to resume.
122 +
123 + Returns:
124 + dict: A dictionary containing the success status and a message.
125 + """
126 + if self.connector_url is None or self.connector_username is None or self.connector_password is None:
127 + return {"message": "Failed to collect Graylog details", "success": False}
128 +
129 + resume_stream = self._resume_stream(stream_id)
130 +
131 + if resume_stream["success"]:
132 + return resume_stream
133 +
134 + def _resume_stream(self, stream_id: str) -> Dict[str, Union[bool, str]]:
135 + """
136 + Resumes a stream in Graylog.
137 +
138 + Args:
139 + stream_id (str): The ID of the stream to resume.
140 +
141 + Returns:
142 + dict: A dictionary containing the success status and a message.
143 + """
144 + try:
145 + requests.post(
146 + f"{self.connector_url}/api/streams/{stream_id}/resume",
147 + headers=self.HEADERS,
148 + auth=(self.connector_username, self.connector_password),
149 + verify=False,
150 + )
151 + return {
152 + "message": "Successfully resumed stream",
153 + "success": True,
154 + }
155 + except Exception as e:
156 + logger.error(f"Failed to resume stream: {e}")
157 + return {"message": "Failed to resume stream", "success": False}
backend/app/services/Velociraptor/artifacts.py
+162 -26
@@ -1,3 +1,7 @@
1 +from typing import Any
2 +from typing import Dict
3 +from typing import Optional
4 +
5 from loguru import logger
6
7 from app.models.agents import AgentMetadata
@@ -9,6 +13,11 @@ class ArtifactsService:
13 A service class that encapsulates the logic for pulling artifacts from Velociraptor.
14 """
15
16 + QUARANTINE_ARTIFACTS = {
17 + "linux": "Linux.Remediation.Quarantine",
18 + "windows": "Windows.Remediation.Quarantine",
19 + }
20 +
21 def __init__(self):
22 self.universal_service = UniversalService()
23
@@ -24,7 +33,7 @@ class ArtifactsService:
33 """
34 return query
35
27 - def _get_artifact_key(self, client_id: str, artifact: str, command: str = None) -> str:
36 + def _get_artifact_key(self, client_id: str, artifact: str, command: str = None, quarantined: bool = None) -> str:
37 """
38 Construct the artifact key.
39
@@ -32,12 +41,18 @@ class ArtifactsService:
41 client_id (str): The ID of the client.
42 artifact (str): The name of the artifact.
43 command (str): The command that was run, if applicable.
44 + quarantined (bool): Whether the client is quarantined or not.
45
46 Returns:
47 str: The constructed artifact key.
48 """
49 + logger.info(f"Quarantined: {quarantined}")
50 if command:
51 return f"collect_client(client_id='{client_id}', urgent=true, artifacts=['{artifact}'], env=dict(Command='{command}'))"
52 + elif quarantined is True:
53 + return f'collect_client(client_id="{client_id}", artifacts=["{artifact}"], spec=dict(`{artifact}`=dict()))'
54 + elif quarantined is False:
55 + return f'collect_client(client_id="{client_id}", artifacts=["{artifact}"], spec=dict(`{artifact}`=dict(`RemovePolicy`="Y")))'
56 else:
57 return f"collect_client(client_id='{client_id}', artifacts=['{artifact}'])"
58
@@ -73,32 +88,27 @@ class ArtifactsService:
88 "artifacts": filtered_artifacts,
89 }
90
76 - def collect_artifacts_linux(self) -> dict:
77 - """
78 - Collect the artifacts from Velociraptor that have a name beginning with `Linux`.
79 -
80 - Returns:
81 - dict: A dictionary with the success status, a message, and potentially the artifacts.
91 + def collect_artifacts_filtered(self, filter_os: str) -> dict:
92 """
83 - return self.collect_artifacts_prefixed("Linux.")
93 + Collect the artifacts from Velociraptor based on the provided OS filter.
94
85 - def collect_artifacts_windows(self) -> dict:
86 - """
87 - Collect the artifacts from Velociraptor that have a name beginning with `Windows`.
95 + Args:
96 + filter_os (str): The OS filter to collect the artifacts.
97
98 Returns:
99 dict: A dictionary with the success status, a message, and potentially the artifacts.
100 """
92 - return self.collect_artifacts_prefixed("Windows.")
101 + os_prefix_map = {"linux": "Linux.", "windows": "Windows.", "macos": "MacOS."}
102
94 - def collect_artifacts_macos(self) -> dict:
95 - """
96 - Collect the artifacts from Velociraptor that have a name beginning with `MacOS`.
103 + prefix = os_prefix_map.get(filter_os.lower())
104
98 - Returns:
99 - dict: A dictionary with the success status, a message, and potentially the artifacts.
100 - """
101 - return self.collect_artifacts_prefixed("MacOS.")
105 + if not prefix:
106 + return {
107 + "success": False,
108 + "message": f"OS filter {filter_os} not supported",
109 + }
110 +
111 + return self.collect_artifacts_prefixed(prefix)
112
113 def collect_artifacts_by_hostname(self, hostname: str) -> dict:
114 """
@@ -117,13 +127,17 @@ class ArtifactsService:
127 "message": f"Agent with hostname {hostname} not found",
128 }
129
120 - os = agent_metadata.os
121 - if "Linux" in os:
122 - return self.collect_artifacts_linux()
123 - elif "Windows" in os:
124 - return self.collect_artifacts_windows()
125 - elif "MacOS" in os:
126 - return self.collect_artifacts_macos()
130 + os = agent_metadata.os.lower()
131 + os_filter_map = {"linux": "Linux", "windows": "Windows", "macos": "MacOS"}
132 +
133 + filter_os = None
134 + for keyword, prefix in os_filter_map.items():
135 + if keyword in os:
136 + filter_os = prefix
137 + break
138 +
139 + if filter_os:
140 + return self.collect_artifacts_filtered(filter_os)
141 else:
142 return {
143 "success": False,
@@ -222,3 +236,125 @@ class ArtifactsService:
236 "message": "Failed to run artifact collection",
237 "success": False,
238 }
239 +
240 + def determine_artifact(self, client_os: str) -> Optional[str]:
241 + """
242 + Determine the artifact to use based on the client's operating system.
243 +
244 + Args:
245 + client_os (str): The operating system of the client.
246 +
247 + Returns:
248 + Optional[str]: The artifact to use, or None if the OS is not supported.
249 + """
250 + client_os = client_os.lower()
251 + return self.QUARANTINE_ARTIFACTS.get(client_os, None)
252 +
253 + def execute_quarantine_query(self, client_id: str, artifact: str, universal_service: Any) -> Dict:
254 + """
255 + Execute the query to quarantine a client.
256 +
257 + Args:
258 + client_id (str): The ID of the client.
259 + artifact (str): The artifact to use for quarantine.
260 + universal_service (Any): The service used to execute the query.
261 +
262 + Returns:
263 + dict: The result of the executed query.
264 + """
265 + query = f'SELECT collect_client(client_id="{client_id}", artifacts=["{artifact}"], spec=dict(`{artifact}`=dict())) FROM scope()'
266 + return universal_service.execute_query(query)
267 +
268 + def execute_quarantine_remove(self, client_id: str, artifact: str, universal_service: Any) -> Dict:
269 + """
270 + Execute the query to remove quarantine from a client.
271 +
272 + Args:
273 + client_id (str): The ID of the client.
274 + artifact (str): The artifact to use for quarantine.
275 + universal_service (Any): The service used to execute the query.
276 +
277 + Returns:
278 + dict: The result of the executed query.
279 + """
280 + query = f'SELECT collect_client(client_id="{client_id}", artifacts=["{artifact}"], spec=dict(`{artifact}`=dict(`RemovePolicy`="Y"))) FROM scope()'
281 + return universal_service.execute_query(query)
282 +
283 + def handle_flow(self, flow: Dict, client_id: str, artifact: str, quarantined: bool) -> str:
284 + """
285 + Handle the flow after executing the quarantine query.
286 +
287 + Args:
288 + flow (dict): The result of the executed query.
289 + client_id (str): The ID of the client.
290 + artifact (str): The artifact to use for quarantine.
291 + universal_service (Any): The service used to watch the flow completion.
292 + quarantined (bool): Whether the client is quarantined or not.
293 +
294 + Returns:
295 + str: The flow ID that was completed.
296 + """
297 + logger.info(f"Quarantined: {quarantined}")
298 + artifact_key = self._get_artifact_key(client_id=client_id, artifact=artifact, quarantined=quarantined)
299 + flow_id = flow["results"][0][artifact_key]["flow_id"]
300 + return self.universal_service.watch_flow_completion(flow_id)
301 +
302 + def execute_action(self, client_id: str, artifact: str, action: str) -> Dict[str, Any]:
303 + """
304 + Execute the given action on the client.
305 +
306 + Args:
307 + client_id (str): The ID of the client.
308 + artifact (str): The artifact to use for the action.
309 + action (str): The action to be performed ("quarantine" or "removequarantine").
310 +
311 + Returns:
312 + dict: A dictionary with the success status and a message.
313 + """
314 + try:
315 + if action == "quarantine":
316 + flow = self.execute_quarantine_query(client_id, artifact, self.universal_service)
317 + completed = self.handle_flow(flow, client_id, artifact, quarantined=True)
318 + elif action == "removequarantine":
319 + flow = self.execute_quarantine_remove(client_id, artifact, self.universal_service)
320 + completed = self.handle_flow(flow, client_id, artifact, quarantined=False)
321 + else:
322 + return {"message": "Action not supported", "success": False}
323 +
324 + logger.info(f"Successfully ran artifact collection on {flow}")
325 +
326 + logger.info(f"Successfully watched flow completion on {completed}")
327 +
328 + return {
329 + "message": f"Successfully {action}d endpoint",
330 + "success": True,
331 + }
332 +
333 + except Exception as err:
334 + logger.error(f"Failed to {action} endpoint: {err}")
335 + return {
336 + "message": f"Failed to {action} endpoint",
337 + "success": False,
338 + }
339 +
340 + def quarantine_endpoint(self, client_id: str, client_os: str, action: str) -> Dict[str, Any]:
341 + """
342 + Quarantine or remove quarantine from an endpoint based on its client ID and operating system.
343 +
344 + Args:
345 + client_id (str): The ID of the client.
346 + client_os (str): The operating system of the client.
347 + action (str): The action to be performed ("quarantine" or "removequarantine").
348 +
349 + Returns:
350 + dict: A dictionary with the success status and a message.
351 + """
352 + artifact = self.determine_artifact(client_os)
353 +
354 + if artifact is None:
355 + return {
356 + "message": f"OS {client_os} not supported",
357 + "success": False,
358 + }
359 +
360 + return self.execute_action(client_id, artifact, action)
backend/app/services/Velociraptor/universal.py
+3 -1
@@ -3,6 +3,7 @@ from datetime import datetime
3
4 import grpc
5 import pyvelociraptor
6 +from loguru import logger
7 from pyvelociraptor import api_pb2
8 from pyvelociraptor import api_pb2_grpc
9
@@ -116,6 +117,7 @@ class UniversalService:
117 "results": results,
118 }
119 except Exception as e:
120 + logger.error(f"Failed to execute query: {e}")
121 return {
122 "success": False,
123 "message": f"Failed to execute query: {e}",
@@ -194,7 +196,7 @@ class UniversalService:
196 """
197 # Formulate queries
198 try:
197 - vql_client_id = f"select client_id from clients(search='host:{client_name}')"
199 + vql_client_id = f"select client_id,os_info from clients(search='host:{client_name}')"
200 vql_last_seen_at = f"select last_seen_at from clients(search='host:{client_name}')"
201
202 # Get the last seen timestamp
backend/app/static/swagger.json
+621 -256
@@ -1742,7 +1742,7 @@
1742 }
1743 },
1744 "/agents/{id}/delete": {
1745 - "post": {
1745 + "delete": {
1746 "tags": ["Agents"],
1747 "summary": "Deletes an agent",
1748 "description": "Deletes an agent from the database.",
@@ -2155,21 +2155,579 @@
2155 }
2156 }
2157 }
2158 - },
2159 - "operationId": "getGraylogIndices",
2160 - "tags": ["Graylog"]
2158 + },
2159 + "operationId": "getGraylogIndices",
2160 + "tags": ["Graylog"]
2161 + }
2162 + },
2163 + "/graylog/indices/{index_name}/delete": {
2164 + "delete": {
2165 + "tags": ["Graylog"],
2166 + "summary": "Deletes a Graylog index",
2167 + "description": "Endpoint to delete a Graylog index.",
2168 + "parameters": [
2169 + {
2170 + "name": "index_name",
2171 + "in": "path",
2172 + "description": "The name of the index to be deleted.",
2173 + "required": true,
2174 + "type": "string"
2175 + }
2176 + ],
2177 + "responses": {
2178 + "200": {
2179 + "description": "Successful operation",
2180 + "schema": {
2181 + "type": "object",
2182 + "properties": {
2183 + "message": {
2184 + "type": "string"
2185 + },
2186 + "success": {
2187 + "type": "boolean"
2188 + }
2189 + }
2190 + }
2191 + },
2192 + "400": {
2193 + "description": "Bad request",
2194 + "schema": {
2195 + "type": "object",
2196 + "properties": {
2197 + "message": {
2198 + "type": "string"
2199 + },
2200 + "success": {
2201 + "type": "boolean"
2202 + }
2203 + }
2204 + }
2205 + },
2206 + "404": {
2207 + "description": "Index not found",
2208 + "schema": {
2209 + "type": "object",
2210 + "properties": {
2211 + "message": {
2212 + "type": "string"
2213 + },
2214 + "success": {
2215 + "type": "boolean"
2216 + }
2217 + }
2218 + }
2219 + }
2220 + }
2221 + }
2222 + },
2223 + "/graylog/inputs": {
2224 + "get": {
2225 + "summary": "Get inputs from Graylog",
2226 + "description": "Endpoint to get inputs from Graylog.",
2227 + "responses": {
2228 + "200": {
2229 + "description": "Successful operation",
2230 + "content": {
2231 + "application/json": {
2232 + "schema": {
2233 + "type": "object",
2234 + "properties": {
2235 + "inputs": {
2236 + "type": "array",
2237 + "items": {
2238 + "type": "object",
2239 + "description": "Input details"
2240 + }
2241 + }
2242 + }
2243 + }
2244 + }
2245 + }
2246 + },
2247 + "default": {
2248 + "description": "Unexpected error",
2249 + "content": {
2250 + "application/json": {
2251 + "schema": {
2252 + "$ref": "#/components/schemas/Error"
2253 + }
2254 + }
2255 + }
2256 + }
2257 + },
2258 + "operationId": "getGraylogInputs",
2259 + "tags": ["Graylog"]
2260 + }
2261 + },
2262 + "/graylog/inputs/running": {
2263 + "get": {
2264 + "summary": "Get running inputs from Graylog",
2265 + "description": "Endpoint to get running inputs from Graylog.",
2266 + "responses": {
2267 + "200": {
2268 + "description": "Successful operation",
2269 + "content": {
2270 + "application/json": {
2271 + "schema": {
2272 + "type": "object",
2273 + "properties": {
2274 + "inputs": {
2275 + "type": "array",
2276 + "items": {
2277 + "type": "object",
2278 + "description": "Input details"
2279 + }
2280 + }
2281 + }
2282 + }
2283 + }
2284 + }
2285 + },
2286 + "default": {
2287 + "description": "Unexpected error",
2288 + "content": {
2289 + "application/json": {
2290 + "schema": {
2291 + "$ref": "#/components/schemas/Error"
2292 + }
2293 + }
2294 + }
2295 + }
2296 + },
2297 + "operationId": "getGraylogInputsRunning",
2298 + "tags": ["Graylog"]
2299 + }
2300 + },
2301 + "/graylog/inputs/configured": {
2302 + "get": {
2303 + "summary": "Get configured inputs from Graylog",
2304 + "description": "Endpoint to get configured inputs from Graylog.",
2305 + "responses": {
2306 + "200": {
2307 + "description": "Successful operation",
2308 + "content": {
2309 + "application/json": {
2310 + "schema": {
2311 + "type": "object",
2312 + "properties": {
2313 + "inputs": {
2314 + "type": "array",
2315 + "items": {
2316 + "type": "object",
2317 + "description": "Input details"
2318 + }
2319 + }
2320 + }
2321 + }
2322 + }
2323 + }
2324 + },
2325 + "default": {
2326 + "description": "Unexpected error",
2327 + "content": {
2328 + "application/json": {
2329 + "schema": {
2330 + "$ref": "#/components/schemas/Error"
2331 + }
2332 + }
2333 + }
2334 + }
2335 + },
2336 + "operationId": "getGraylogInputsConfigured",
2337 + "tags": ["Graylog"]
2338 + }
2339 + },
2340 + "/graylog/inputs/{input_id}/stop": {
2341 + "delete": {
2342 + "tags": ["Graylog"],
2343 + "summary": "Stops a Graylog input",
2344 + "description": "Endpoint to stop a Graylog input.",
2345 + "parameters": [
2346 + {
2347 + "name": "input_id",
2348 + "in": "path",
2349 + "description": "The ID of the input to be stopped.",
2350 + "required": true,
2351 + "type": "string"
2352 + }
2353 + ],
2354 + "responses": {
2355 + "200": {
2356 + "description": "Successful operation",
2357 + "schema": {
2358 + "type": "object",
2359 + "properties": {
2360 + "message": {
2361 + "type": "string"
2362 + },
2363 + "success": {
2364 + "type": "boolean"
2365 + }
2366 + }
2367 + }
2368 + },
2369 + "400": {
2370 + "description": "Bad request",
2371 + "schema": {
2372 + "type": "object",
2373 + "properties": {
2374 + "message": {
2375 + "type": "string"
2376 + },
2377 + "success": {
2378 + "type": "boolean"
2379 + }
2380 + }
2381 + }
2382 + },
2383 + "404": {
2384 + "description": "Input not found",
2385 + "schema": {
2386 + "type": "object",
2387 + "properties": {
2388 + "message": {
2389 + "type": "string"
2390 + },
2391 + "success": {
2392 + "type": "boolean"
2393 + }
2394 + }
2395 + }
2396 + }
2397 + }
2398 + }
2399 + },
2400 + "/graylog/inputs/{input_id}/start": {
2401 + "put": {
2402 + "tags": ["Graylog"],
2403 + "summary": "Starts a Graylog input",
2404 + "description": "Endpoint to start a Graylog input.",
2405 + "parameters": [
2406 + {
2407 + "name": "input_id",
2408 + "in": "path",
2409 + "description": "The ID of the input to be started.",
2410 + "required": true,
2411 + "type": "string"
2412 + }
2413 + ],
2414 + "responses": {
2415 + "200": {
2416 + "description": "Successful operation",
2417 + "schema": {
2418 + "type": "object",
2419 + "properties": {
2420 + "message": {
2421 + "type": "string"
2422 + },
2423 + "success": {
2424 + "type": "boolean"
2425 + }
2426 + }
2427 + }
2428 + },
2429 + "400": {
2430 + "description": "Bad request",
2431 + "schema": {
2432 + "type": "object",
2433 + "properties": {
2434 + "message": {
2435 + "type": "string"
2436 + },
2437 + "success": {
2438 + "type": "boolean"
2439 + }
2440 + }
2441 + }
2442 + },
2443 + "404": {
2444 + "description": "Input not found",
2445 + "schema": {
2446 + "type": "object",
2447 + "properties": {
2448 + "message": {
2449 + "type": "string"
2450 + },
2451 + "success": {
2452 + "type": "boolean"
2453 + }
2454 + }
2455 + }
2456 + }
2457 + }
2458 + }
2459 + },
2460 + "/graylog/inputs/{input_id}/state": {
2461 + "get": {
2462 + "tags": ["Graylog"],
2463 + "summary": "Get the state of a Graylog input",
2464 + "description": "Endpoint to get the state of a Graylog input.",
2465 + "parameters": [
2466 + {
2467 + "name": "input_id",
2468 + "in": "path",
2469 + "description": "The ID of the input to get the state of.",
2470 + "required": true,
2471 + "type": "string"
2472 + }
2473 + ],
2474 + "responses": {
2475 + "200": {
2476 + "description": "Successful operation",
2477 + "schema": {
2478 + "type": "object",
2479 + "properties": {
2480 + "state": {
2481 + "type": "string"
2482 + }
2483 + }
2484 + }
2485 + },
2486 + "400": {
2487 + "description": "Bad request",
2488 + "schema": {
2489 + "type": "object",
2490 + "properties": {
2491 + "message": {
2492 + "type": "string"
2493 + }
2494 + }
2495 + }
2496 + },
2497 + "404": {
2498 + "description": "Input not found",
2499 + "schema": {
2500 + "type": "object",
2501 + "properties": {
2502 + "message": {
2503 + "type": "string"
2504 + }
2505 + }
2506 + }
2507 + }
2508 + }
2509 + }
2510 + },
2511 + "/graylog/event/definitions": {
2512 + "get": {
2513 + "summary": "Get event definitions from Graylog",
2514 + "description": "Endpoint to get event definitions from Graylog.",
2515 + "responses": {
2516 + "200": {
2517 + "description": "Successful operation",
2518 + "schema": {
2519 + "type": "object",
2520 + "properties": {
2521 + "event_definitions": {
2522 + "type": "array",
2523 + "items": {
2524 + "type": "object",
2525 + "description": "Event definition details"
2526 + }
2527 + }
2528 + }
2529 + }
2530 + },
2531 + "default": {
2532 + "description": "Unexpected error",
2533 + "schema": {
2534 + "$ref": "#/components/schemas/Error"
2535 + }
2536 + }
2537 + },
2538 + "operationId": "getGraylogEventDefinitions",
2539 + "tags": ["Graylog"]
2540 + }
2541 + },
2542 + "/graylog/event/alerts": {
2543 + "get": {
2544 + "summary": "Get event alerts from Graylog",
2545 + "description": "Endpoint to get event alerts from Graylog.",
2546 + "responses": {
2547 + "200": {
2548 + "description": "Successful operation",
2549 + "schema": {
2550 + "type": "object",
2551 + "properties": {
2552 + "event_alerts": {
2553 + "type": "array",
2554 + "items": {
2555 + "type": "object",
2556 + "description": "Event alert details"
2557 + }
2558 + }
2559 + }
2560 + }
2561 + },
2562 + "default": {
2563 + "description": "Unexpected error",
2564 + "schema": {
2565 + "$ref": "#/components/schemas/Error"
2566 + }
2567 + }
2568 + },
2569 + "operationId": "getGraylogEventAlerts",
2570 + "tags": ["Graylog"]
2571 + }
2572 + },
2573 + "/graylog/pipeline/rules": {
2574 + "get": {
2575 + "summary": "Get pipeline rules from Graylog",
2576 + "description": "Endpoint to get pipeline rules from Graylog.",
2577 + "responses": {
2578 + "200": {
2579 + "description": "Successful operation",
2580 + "schema": {
2581 + "type": "object",
2582 + "properties": {
2583 + "pipeline_rules": {
2584 + "type": "array",
2585 + "items": {
2586 + "type": "object",
2587 + "description": "Pipeline rule details"
2588 + }
2589 + }
2590 + }
2591 + }
2592 + },
2593 + "default": {
2594 + "description": "Unexpected error",
2595 + "schema": {
2596 + "$ref": "#/components/schemas/Error"
2597 + }
2598 + }
2599 + },
2600 + "operationId": "getGraylogPipelineRules",
2601 + "tags": ["Graylog"]
2602 + }
2603 + },
2604 + "/graylog/pipeline/pipelines": {
2605 + "get": {
2606 + "summary": "Get pipelines from Graylog",
2607 + "description": "Endpoint to get pipelines from Graylog.",
2608 + "responses": {
2609 + "200": {
2610 + "description": "Successful operation",
2611 + "schema": {
2612 + "type": "object",
2613 + "properties": {
2614 + "pipelines": {
2615 + "type": "array",
2616 + "items": {
2617 + "type": "object",
2618 + "description": "Pipeline details"
2619 + }
2620 + }
2621 + }
2622 + }
2623 + },
2624 + "default": {
2625 + "description": "Unexpected error",
2626 + "schema": {
2627 + "$ref": "#/components/schemas/Error"
2628 + }
2629 + }
2630 + },
2631 + "operationId": "getGraylogPipelines",
2632 + "tags": ["Graylog"]
2633 + }
2634 + },
2635 + "/graylog/streams": {
2636 + "get": {
2637 + "summary": "Get streams from Graylog",
2638 + "description": "Endpoint to get streams from Graylog.",
2639 + "responses": {
2640 + "200": {
2641 + "description": "Successful operation",
2642 + "schema": {
2643 + "type": "array",
2644 + "items": {
2645 + "type": "object",
2646 + "description": "Stream details"
2647 + }
2648 + }
2649 + },
2650 + "default": {
2651 + "description": "Unexpected error",
2652 + "schema": {
2653 + "$ref": "#/components/schemas/Error"
2654 + }
2655 + }
2656 + },
2657 + "operationId": "getGraylogStreams",
2658 + "tags": ["Graylog"]
2659 + }
2660 + },
2661 + "/graylog/streams/{stream_id}/pause": {
2662 + "post": {
2663 + "tags": ["Graylog"],
2664 + "summary": "Pauses a Graylog stream",
2665 + "description": "Endpoint to pause a Graylog stream.",
2666 + "parameters": [
2667 + {
2668 + "name": "stream_id",
2669 + "in": "path",
2670 + "description": "The ID of the stream to be paused.",
2671 + "required": true,
2672 + "type": "string"
2673 + }
2674 + ],
2675 + "responses": {
2676 + "200": {
2677 + "description": "Successful operation",
2678 + "schema": {
2679 + "type": "object",
2680 + "properties": {
2681 + "message": {
2682 + "type": "string"
2683 + },
2684 + "success": {
2685 + "type": "boolean"
2686 + }
2687 + }
2688 + }
2689 + },
2690 + "400": {
2691 + "description": "Bad request",
2692 + "schema": {
2693 + "type": "object",
2694 + "properties": {
2695 + "message": {
2696 + "type": "string"
2697 + },
2698 + "success": {
2699 + "type": "boolean"
2700 + }
2701 + }
2702 + }
2703 + },
2704 + "404": {
2705 + "description": "Stream not found",
2706 + "schema": {
2707 + "type": "object",
2708 + "properties": {
2709 + "message": {
2710 + "type": "string"
2711 + },
2712 + "success": {
2713 + "type": "boolean"
2714 + }
2715 + }
2716 + }
2717 + }
2718 + }
2719 }
2720 },
2163 - "/graylog/indices/{index_name}/delete": {
2164 - "delete": {
2721 + "/graylog/streams/{stream_id}/resume": {
2722 + "post": {
2723 "tags": ["Graylog"],
2166 - "summary": "Deletes a Graylog index",
2167 - "description": "Endpoint to delete a Graylog index.",
2724 + "summary": "Resumes a Graylog stream",
2725 + "description": "Endpoint to resume a Graylog stream.",
2726 "parameters": [
2727 {
2170 - "name": "index_name",
2728 + "name": "stream_id",
2729 "in": "path",
2172 - "description": "The name of the index to be deleted.",
2730 + "description": "The ID of the stream to be resumed.",
2731 "required": true,
2732 "type": "string"
2733 }
@@ -2204,7 +2762,7 @@
2762 }
2763 },
2764 "404": {
2207 - "description": "Index not found",
2765 + "description": "Stream not found",
2766 "schema": {
2767 "type": "object",
2768 "properties": {
@@ -2220,45 +2778,6 @@
2778 }
2779 }
2780 },
2223 - "/graylog/inputs": {
2224 - "get": {
2225 - "summary": "Get inputs from Graylog",
2226 - "description": "Endpoint to get inputs from Graylog.",
2227 - "responses": {
2228 - "200": {
2229 - "description": "Successful operation",
2230 - "content": {
2231 - "application/json": {
2232 - "schema": {
2233 - "type": "object",
2234 - "properties": {
2235 - "inputs": {
2236 - "type": "array",
2237 - "items": {
2238 - "type": "object",
2239 - "description": "Input details"
2240 - }
2241 - }
2242 - }
2243 - }
2244 - }
2245 - }
2246 - },
2247 - "default": {
2248 - "description": "Unexpected error",
2249 - "content": {
2250 - "application/json": {
2251 - "schema": {
2252 - "$ref": "#/components/schemas/Error"
2253 - }
2254 - }
2255 - }
2256 - }
2257 - },
2258 - "operationId": "getGraylogInputs",
2259 - "tags": ["Graylog"]
2260 - }
2261 - },
2781 "/alerts": {
2782 "post": {
2783 "summary": "Create and get alerts",
@@ -3293,171 +3812,15 @@
3812 "tags": ["Shuffle"]
3813 }
3814 },
3296 - "/velociraptor/artifacts": {
3297 - "get": {
3298 - "summary": "Get all artifacts",
3299 - "description": "Endpoint to get all artifacts.",
3300 - "responses": {
3301 - "200": {
3302 - "description": "Successful operation",
3303 - "content": {
3304 - "application/json": {
3305 - "schema": {
3306 - "type": "object",
3307 - "properties": {
3308 - "artifacts": {
3309 - "type": "array",
3310 - "items": {
3311 - "type": "object",
3312 - "description": "Artifact details"
3313 - }
3314 - }
3315 - }
3316 - }
3317 - }
3318 - }
3319 - },
3320 - "default": {
3321 - "description": "Unexpected error",
3322 - "content": {
3323 - "application/json": {
3324 - "schema": {
3325 - "$ref": "#/components/schemas/Error"
3326 - }
3327 - }
3328 - }
3329 - }
3330 - },
3331 - "operationId": "getAllArtifacts",
3332 - "tags": ["Velociraptor"]
3333 - }
3334 - },
3335 - "/velociraptor/artifacts/linux": {
3336 - "get": {
3337 - "summary": "Get all linux artifacts",
3338 - "description": "Endpoint to get all linux artifacts.",
3339 - "responses": {
3340 - "200": {
3341 - "description": "Successful operation",
3342 - "content": {
3343 - "application/json": {
3344 - "schema": {
3345 - "type": "object",
3346 - "properties": {
3347 - "artifacts": {
3348 - "type": "array",
3349 - "items": {
3350 - "type": "object",
3351 - "description": "Artifact details"
3352 - }
3353 - }
3354 - }
3355 - }
3356 - }
3357 - }
3358 - },
3359 - "default": {
3360 - "description": "Unexpected error",
3361 - "content": {
3362 - "application/json": {
3363 - "schema": {
3364 - "$ref": "#/components/schemas/Error"
3365 - }
3366 - }
3367 - }
3368 - }
3369 - },
3370 - "operationId": "getAllLinuxArtifacts",
3371 - "tags": ["Velociraptor"]
3372 - }
3373 - },
3374 - "/velociraptor/artifacts/windows": {
3375 - "get": {
3376 - "summary": "Get all windows artifacts",
3377 - "description": "Endpoint to get all windows artifacts.",
3378 - "responses": {
3379 - "200": {
3380 - "description": "Successful operation",
3381 - "content": {
3382 - "application/json": {
3383 - "schema": {
3384 - "type": "object",
3385 - "properties": {
3386 - "artifacts": {
3387 - "type": "array",
3388 - "items": {
3389 - "type": "object",
3390 - "description": "Artifact details"
3391 - }
3392 - }
3393 - }
3394 - }
3395 - }
3396 - }
3397 - },
3398 - "default": {
3399 - "description": "Unexpected error",
3400 - "content": {
3401 - "application/json": {
3402 - "schema": {
3403 - "$ref": "#/components/schemas/Error"
3404 - }
3405 - }
3406 - }
3407 - }
3408 - },
3409 - "operationId": "getAllWindowsArtifacts",
3410 - "tags": ["Velociraptor"]
3411 - }
3412 - },
3413 - "/velociraptor/artifacts/mac": {
3414 - "get": {
3415 - "summary": "Get all mac artifacts",
3416 - "description": "Endpoint to get all mac artifacts.",
3417 - "responses": {
3418 - "200": {
3419 - "description": "Successful operation",
3420 - "content": {
3421 - "application/json": {
3422 - "schema": {
3423 - "type": "object",
3424 - "properties": {
3425 - "artifacts": {
3426 - "type": "array",
3427 - "items": {
3428 - "type": "object",
3429 - "description": "Artifact details"
3430 - }
3431 - }
3432 - }
3433 - }
3434 - }
3435 - }
3436 - },
3437 - "default": {
3438 - "description": "Unexpected error",
3439 - "content": {
3440 - "application/json": {
3441 - "schema": {
3442 - "$ref": "#/components/schemas/Error"
3443 - }
3444 - }
3445 - }
3446 - }
3447 - },
3448 - "operationId": "getAllMacArtifacts",
3449 - "tags": ["Velociraptor"]
3450 - }
3451 - },
3452 - "/velociraptor/artifacts/{hostname}": {
3815 + "/velociraptor/artifacts/os/{filter_os}": {
3816 "get": {
3454 - "summary": "Get all artifacts for a hostname",
3455 - "description": "Endpoint to get all artifacts for a hostname.",
3817 + "summary": "Get artifacts filtered by OS",
3818 + "description": "Endpoint to get artifacts based on the OS filter.",
3819 "parameters": [
3820 {
3458 - "name": "hostname",
3821 + "name": "filter_os",
3822 "in": "path",
3460 - "description": "The client name",
3823 + "description": "Operating system filter for the artifacts.",
3824 "required": true,
3825 "schema": {
3826 "type": "string"
@@ -3495,28 +3858,43 @@
3858 }
3859 }
3860 },
3498 - "operationId": "getAllArtifactsForHostname",
3861 + "operationId": "getFilteredArtifacts",
3862 "tags": ["Velociraptor"]
3863 }
3864 },
3502 - "/velociraptor/artifacts/collection": {
3865 + "/velociraptor/operation": {
3866 "post": {
3504 - "summary": "Create a new artifact collection",
3505 - "description": "Endpoint to create a new artifact collection.",
3867 + "summary": "Execute an operation",
3868 + "description": "Endpoint to execute an operation like artifact collection, remote command execution, or quarantine.",
3869 "requestBody": {
3507 - "description": "Artifact collection details",
3870 + "description": "Operation details",
3871 "content": {
3872 "application/json": {
3873 "schema": {
3874 "type": "object",
3875 "properties": {
3513 - "artifact_name": {
3876 + "client_name": {
3877 "type": "string",
3515 - "description": "The name of the artifact collection."
3878 + "description": "The hostname of the client."
3879 },
3517 - "client_name": {
3880 + "operation": {
3881 + "type": "string",
3882 + "description": "The operation to perform. Valid values are 'collect_artifact', 'run_command', and 'quarantine'."
3883 + },
3884 + "action": {
3885 + "type": "string",
3886 + "description": "The action to perform for quarantine. Valid values are 'quarantine' and 'removequarantine'.",
3887 + "nullable": true
3888 + },
3889 + "command": {
3890 + "type": "string",
3891 + "description": "The command to run for remote command execution.",
3892 + "nullable": true
3893 + },
3894 + "artifact_name": {
3895 "type": "string",
3519 - "description": "The name of the client to collect the artifact for."
3896 + "description": "The name of the artifact for artifact collection or remote command execution.",
3897 + "nullable": true
3898 }
3899 }
3900 }
@@ -3531,9 +3909,9 @@
3909 "schema": {
3910 "type": "object",
3911 "properties": {
3534 - "collection": {
3912 + "result": {
3913 "type": "object",
3536 - "description": "Artifact collection details"
3914 + "description": "Operation result details"
3915 }
3916 }
3917 }
@@ -3551,41 +3929,25 @@
3929 }
3930 }
3931 },
3554 - "operationId": "createArtifactCollection",
3932 + "operationId": "executeOperation",
3933 "tags": ["Velociraptor"]
3934 }
3935 },
3558 - "/velociraptor/remotecommand": {
3559 - "post": {
3560 - "summary": "Run a remote command",
3561 - "description": "Endpoint to run a remote command.",
3562 - "requestBody": {
3563 - "description": "Remote command details",
3564 - "content": {
3565 - "application/json": {
3566 - "schema": {
3567 - "type": "object",
3568 - "properties": {
3569 - "client_name": {
3570 - "type": "string",
3571 - "value": "WIN-39O01J5F7G5",
3572 - "description": "The hostname of the client to run the command on."
3573 - },
3574 - "artifact_name": {
3575 - "type": "string",
3576 - "value": "Windows.System.PowerShell",
3577 - "description": "The name of the artifact to run."
3578 - },
3579 - "command": {
3580 - "type": "string",
3581 - "value": "ping 8.8.8.8",
3582 - "description": "The powershell command to run."
3583 - }
3584 - }
3585 - }
3936 + "/velociraptor/artifacts/hostname/{hostname}": {
3937 + "get": {
3938 + "summary": "Get all artifacts for a specific hostname",
3939 + "description": "Endpoint to get all artifacts available for a specific hostname.",
3940 + "parameters": [
3941 + {
3942 + "name": "hostname",
3943 + "in": "path",
3944 + "description": "The hostname for which to collect artifacts.",
3945 + "required": true,
3946 + "schema": {
3947 + "type": "string"
3948 }
3949 }
3588 - },
3950 + ],
3951 "responses": {
3952 "200": {
3953 "description": "Successful operation",
@@ -3594,9 +3956,12 @@
3956 "schema": {
3957 "type": "object",
3958 "properties": {
3597 - "output": {
3598 - "type": "string",
3599 - "description": "The output of the command."
3959 + "artifacts": {
3960 + "type": "array",
3961 + "items": {
3962 + "type": "object",
3963 + "description": "Artifact details"
3964 + }
3965 }
3966 }
3967 }
@@ -3614,7 +3979,7 @@
3979 }
3980 }
3981 },
3617 - "operationId": "runPowershellCommand",
3982 + "operationId": "getAllArtifactsForHostname",
3983 "tags": ["Velociraptor"]
3984 }
3985 },
index.html
+1
@@ -5,6 +5,7 @@
5 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6 <link rel="shortcut icon" href="/socfortress_favicon.ico" />
7 <title>SOCFortress CoPilot</title>
8 + <meta name="viewport" content="width=device-width, initial-scale=1.0" />
9 <meta name="description" content="Your OpenSource Security Assistant" />
10 <meta
11 name="keywords"
src/api/agents.ts
+6 -7
@@ -1,13 +1,10 @@
1 import { FlaskBaseResponse } from "@/types/flask"
2 import { HttpClient } from "./httpClient"
3 -import { Agents, AgentVulnerabilities, OutdatedWazuhAgents, OutdatedVelociraptorAgents } from "@/types/agents" // Import the new types
3 +import { Agent, AgentVulnerabilities, OutdatedWazuhAgents, OutdatedVelociraptorAgents } from "@/types/agents"
4
5 export default {
6 - getAgents() {
7 - return HttpClient.get<FlaskBaseResponse & { agents: Agents[] }>("/agents")
8 - },
9 - getAgent(id: string) {
10 - return HttpClient.get<FlaskBaseResponse & { agent: Agents }>(`/agents/${id}`) // Should be Agents, not Agents[]
6 + getAgents(id?: string) {
7 + return HttpClient.get<FlaskBaseResponse & { agent?: Agent; agents?: Agent[] }>(`/agents${id ? "/" + id : ""}`)
8 },
9 markCritical(id: string) {
10 return HttpClient.post<FlaskBaseResponse>(`/agents/${id}/critical`)
@@ -22,11 +19,13 @@ export default {
19 return HttpClient.post<FlaskBaseResponse>(`/agents/sync`)
20 },
21 agentVulnerabilities(id: string) {
25 - return HttpClient.get<FlaskBaseResponse & { vulnerabilities: AgentVulnerabilities[] }>(`/agents/${id}/vulnerabilities`) // Include the vulnerabilities
22 + return HttpClient.get<FlaskBaseResponse & { vulnerabilities: AgentVulnerabilities[] }>(`/agents/${id}/vulnerabilities`)
23 },
24 + // IGNORE AT THE MOMENT !
25 agentsWazuhOutdated() {
26 return HttpClient.get<FlaskBaseResponse & { outdated_wazuh_agents: OutdatedWazuhAgents }>(`/agents/wazuh/outdated`) // Include the outdated Wazuh agents
27 },
28 + // IGNORE AT THE MOMENT !
29 agentsVelociraptorOutdated() {
30 return HttpClient.get<FlaskBaseResponse & { outdated_velociraptor_agents: OutdatedVelociraptorAgents }>(
31 `/agents/velociraptor/outdated`
src/api/graylog.ts new
+42
@@ -0,0 +1,42 @@
1 +import { HttpClient } from "./httpClient"
2 +import { FlaskBaseResponse } from "@/types/flask"
3 +import { Message, ThroughputMetric, IndexData, Inputs, InputState, Streams } from "@/types/graylog" // Import Graylog interfaces
4 +
5 +export default {
6 + getMessages() {
7 + return HttpClient.get<FlaskBaseResponse & { messages: Message[] }>(`/graylog/messages`)
8 + },
9 + getMetrics() {
10 + return HttpClient.get<FlaskBaseResponse & { metrics: ThroughputMetric[] }>(`/graylog/metrics`)
11 + },
12 + getIndices() {
13 + return HttpClient.get<FlaskBaseResponse & { indexData: IndexData }>(`/graylog/indices`)
14 + },
15 + deleteIndex(indexName: string) {
16 + return HttpClient.delete<FlaskBaseResponse>(`/graylog/indices/${indexName}/delete`)
17 + },
18 + getInputsRunning() {
19 + return HttpClient.get<FlaskBaseResponse & { inputs: Inputs }>(`/graylog/inputs/running`)
20 + },
21 + getInputsConfigured() {
22 + return HttpClient.get<FlaskBaseResponse & { inputs: Inputs }>(`/graylog/inputs/configured`)
23 + },
24 + startInput(inputId: string) {
25 + return HttpClient.put<FlaskBaseResponse>(`/graylog/inputs/${inputId}/start`)
26 + },
27 + stopInput(inputId: string) {
28 + return HttpClient.delete<FlaskBaseResponse>(`/graylog/inputs/${inputId}/stop`)
29 + },
30 + getInputState(inputId: string) {
31 + return HttpClient.get<FlaskBaseResponse & { state: InputState }>(`/graylog/inputs/${inputId}/state`)
32 + },
33 + getStreams() {
34 + return HttpClient.get<FlaskBaseResponse & { streams: Streams }>(`/graylog/streams`)
35 + },
36 + stopStream(streamId: string) {
37 + return HttpClient.post<FlaskBaseResponse>(`/graylog/streams/${streamId}/pause`)
38 + },
39 + startStream(streamId: string) {
40 + return HttpClient.post<FlaskBaseResponse>(`/graylog/streams/${streamId}/resume`)
41 + }
42 +}
src/api/index.ts
+3 -1
@@ -1,9 +1,11 @@
1 import connectors from "./connectors"
2 import indices from "./indices"
3 import agents from "./agents"
4 +import graylog from "./graylog"
5
6 export default {
7 connectors,
8 indices,
8 - agents
9 + agents,
10 + graylog
11 }
src/assets/images/cortex.svg new
+40
@@ -0,0 +1,40 @@
1 +<?xml version="1.0" standalone="no"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3 + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4 +<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5 + width="184.000000pt" height="158.000000pt" viewBox="0 0 184.000000 158.000000"
6 + preserveAspectRatio="xMidYMid meet">
7 +
8 +<g transform="translate(0.000000,158.000000) scale(0.100000,-0.100000)"
9 +fill="#000000" stroke="none">
10 +<path d="M0 790 l0 -790 920 0 920 0 0 790 0 790 -920 0 -920 0 0 -790z m797
11 +649 c50 -24 113 -92 155 -166 27 -48 58 -70 58 -41 0 14 -44 91 -77 134 -30
12 +41 -29 50 10 71 46 24 147 21 222 -7 89 -34 118 -68 151 -178 31 -104 69 -157
13 +122 -172 26 -7 34 -6 39 6 3 8 4 16 2 17 -2 2 -21 10 -42 20 -40 18 -59 50
14 +-95 162 -10 33 -23 69 -27 79 -8 17 -5 19 25 12 83 -16 189 -114 241 -223 43
15 +-90 75 -253 50 -253 -5 0 -21 4 -36 10 -14 5 -68 16 -120 24 -110 16 -152 37
16 +-182 92 -25 44 -49 52 -45 15 7 -70 106 -135 221 -146 135 -14 166 -31 182
17 +-104 7 -30 5 -50 -5 -75 -19 -45 -33 -52 -59 -28 -12 11 -41 29 -64 41 -24 12
18 +-49 34 -58 51 -18 34 -35 31 -35 -5 0 -24 60 -85 84 -85 8 0 24 -9 37 -19 l24
19 +-20 -40 -18 c-50 -22 -278 -26 -337 -5 -59 21 -88 54 -88 102 0 46 -16 53 -31
20 +14 -14 -39 5 -87 47 -119 39 -30 37 -37 -16 -54 -20 -6 -83 -38 -140 -71 -110
21 +-63 -142 -71 -172 -43 -31 28 -22 72 28 138 63 83 91 134 99 177 8 44 -1 78
22 +-21 78 -10 0 -14 -13 -14 -47 0 -53 -12 -77 -93 -187 -42 -57 -52 -77 -51
23 +-110 0 -23 3 -49 8 -59 18 -38 -148 -13 -197 31 -52 45 -72 157 -42 229 19 45
24 +19 53 -1 53 -9 0 -23 -17 -32 -40 -22 -51 -75 -75 -156 -68 -93 8 -153 72
25 +-171 185 -7 44 17 153 34 153 5 0 16 -20 25 -44 10 -25 35 -61 57 -81 l40 -36
26 +133 3 133 3 77 -39 c42 -21 79 -36 82 -33 15 15 -9 37 -73 69 -67 33 -70 33
27 +-205 33 l-137 0 -30 29 c-88 84 -78 225 22 333 36 38 47 35 47 -15 0 -48 39
28 +-129 81 -169 63 -59 94 -66 270 -58 188 9 197 6 326 -81 52 -35 119 -72 150
29 +-84 65 -24 138 -24 138 0 0 11 -17 17 -63 22 -64 7 -151 48 -217 101 -30 25
30 +-30 25 -13 59 9 19 35 56 58 83 68 78 74 91 72 159 -1 70 -24 140 -44 133 -15
31 +-5 -14 -10 3 -87 15 -69 9 -97 -36 -152 -38 -47 -92 -125 -97 -140 -2 -5 -27
32 +0 -56 11 -60 24 -117 26 -252 10 -90 -11 -98 -11 -148 11 -66 29 -107 75 -128
33 +146 -32 110 8 199 94 211 54 7 73 -9 102 -88 13 -34 35 -70 52 -84 32 -27 102
34 +-50 125 -41 24 10 9 31 -28 37 -60 10 -80 28 -116 106 -38 82 -37 89 22 125
35 +39 24 89 23 142 -3z m583 -881 c0 -13 -9 -41 -19 -63 -17 -37 -31 -52 -116
36 +-120 -29 -22 -30 -26 -28 -94 l3 -71 -55 0 -55 0 -6 41 c-11 66 -42 102 -99
37 +115 -61 13 -95 30 -95 47 1 18 183 116 265 143 44 15 95 23 138 24 63 0 67 -1
38 +67 -22z"/>
39 +</g>
40 +</svg>
src/assets/scss/element-variables.scss
+5
@@ -132,4 +132,9 @@ $--font-path: "~element-ui/lib/theme-chalk/fonts";
132
133 .el-dialog {
134 min-width: 310px;
135 + max-width: 1500px;
136 +
137 + .el-dialog__body {
138 + box-sizing: border-box;
139 + }
140 }
src/assets/scss/global.scss
+4
@@ -151,3 +151,7 @@ body {
151 padding: 0 10px;
152 }
153 }*/
154 +
155 +[aria-label][data-balloon-pos]:after {
156 + border-radius: 4px;
157 +}
src/components/agents/AgentCard.vue new
+237
@@ -0,0 +1,237 @@
1 +<template>
2 + <div class="agent-card" :class="{ critical: agent.critical_asset }" v-loading="loading">
3 + <div class="wrapper">
4 + <div class="agent-header">
5 + <div class="title">
6 + <el-tooltip :content="`${isOnline ? 'online' : 'last seen'} - ${formatLastSeen}`" placement="top" :show-arrow="false">
7 + <div class="hostname" :class="{ online: isOnline }">{{ agent.hostname }}</div>
8 + </el-tooltip>
9 + <div class="critical" :class="{ active: agent.critical_asset }">
10 + <el-tooltip content="Toggle Critical Assets" placement="top" :show-arrow="false">
11 + <el-button
12 + text
13 + :icon="StarIcon"
14 + :type="agent.critical_asset ? 'warning' : ''"
15 + circle
16 + @click.stop="toggleCritical(agent.agent_id, agent.critical_asset)"
17 + />
18 + </el-tooltip>
19 + </div>
20 + </div>
21 + <div class="info">#{{ agent.agent_id }} / {{ agent.label }}</div>
22 + </div>
23 + <div class="agent-info">
24 + <div class="os" :title="agent.os">
25 + {{ agent.os }}
26 + </div>
27 + <div class="ip-address" :title="agent.ip_address">{{ agent.ip_address }}</div>
28 + </div>
29 +
30 + <div class="agent-actions" v-if="showActions">
31 + <div class="box">
32 + <el-tooltip content="Delete" placement="top" :show-arrow="false">
33 + <el-button type="danger" :icon="DeleteIcon" circle @click.stop="handleDelete" />
34 + </el-tooltip>
35 + </div>
36 + </div>
37 + </div>
38 + </div>
39 +</template>
40 +
41 +<script setup lang="ts">
42 +import { computed, ref, toRefs } from "vue"
43 +import { Agent } from "@/types/agents.d"
44 +import dayjs from "dayjs"
45 +import Api from "@/api"
46 +import { handleDeleteAgent, isAgentOnline, toggleAgentCritical } from "./utils"
47 +import { ElMessage, ElMessageBox } from "element-plus"
48 +import { Star as StarIcon, Delete as DeleteIcon } from "@element-plus/icons-vue"
49 +
50 +const emit = defineEmits<{
51 + (e: "delete"): void
52 +}>()
53 +
54 +const props = defineProps<{
55 + agent: Agent
56 + showActions?: boolean
57 +}>()
58 +const { agent, showActions } = toRefs(props)
59 +
60 +const loading = ref(false)
61 +
62 +const isOnline = computed(() => {
63 + return isAgentOnline(agent.value.last_seen)
64 +})
65 +const formatLastSeen = computed(() => {
66 + const lastSeenDate = dayjs(agent.value.last_seen)
67 + if (!lastSeenDate.isValid()) return agent.value.last_seen
68 +
69 + return lastSeenDate.format("DD/MM/YYYY @ HH:mm")
70 +})
71 +
72 +function handleDelete() {
73 + handleDeleteAgent({
74 + agent: agent.value,
75 + cbBefore: () => {
76 + loading.value = true
77 + },
78 + cbSuccess: () => {
79 + emit("delete")
80 + },
81 + cbAfter: () => {
82 + loading.value = false
83 + }
84 + })
85 +}
86 +
87 +function toggleCritical(agentId: string, criticalStatus: boolean) {
88 + toggleAgentCritical({
89 + agentId,
90 + criticalStatus,
91 + cbBefore: () => {
92 + loading.value = true
93 + },
94 + cbSuccess: () => {
95 + agent.value.critical_asset = !criticalStatus
96 + },
97 + cbAfter: () => {
98 + loading.value = false
99 + }
100 + })
101 +}
102 +</script>
103 +
104 +<style lang="scss" scoped>
105 +@import "@/assets/scss/_variables";
106 +@import "@/assets/scss/card-shadow";
107 +
108 +.agent-card {
109 + container-type: inline-size;
110 + @extend .card-base;
111 + @extend .card-shadow--small;
112 + overflow: hidden;
113 + border: 2px solid transparent;
114 + max-width: 100%;
115 + padding: var(--size-3) var(--size-4);
116 + box-sizing: border-box;
117 + cursor: pointer;
118 + transition: all 0.3s;
119 +
120 + .wrapper {
121 + display: flex;
122 + gap: var(--size-6);
123 + flex-direction: row;
124 + align-items: center;
125 + overflow: hidden;
126 +
127 + .agent-header {
128 + display: flex;
129 + flex-direction: column;
130 + min-width: 300px;
131 +
132 + .title {
133 + display: flex;
134 + align-items: center;
135 + gap: var(--size-2);
136 + margin-bottom: 4px;
137 +
138 + .hostname {
139 + font-weight: bold;
140 + white-space: nowrap;
141 + line-height: 32px;
142 + height: 32px;
143 + border-radius: 4px;
144 + border: 1px solid $text-color-info;
145 + border-color: transparent;
146 + box-sizing: border-box;
147 + overflow: hidden;
148 + text-overflow: ellipsis;
149 +
150 + &.online {
151 + padding: 0px 15px;
152 + color: $text-color-success;
153 + border-color: $text-color-success;
154 + }
155 + }
156 + }
157 + .info {
158 + font-family: var(--font-mono);
159 + font-size: var(--font-size-0);
160 + opacity: 0.7;
161 + white-space: nowrap;
162 + overflow: hidden;
163 + text-overflow: ellipsis;
164 + margin-left: 2px;
165 + }
166 + }
167 +
168 + .agent-info {
169 + display: flex;
170 + flex-direction: column;
171 + flex-grow: 1;
172 + overflow: hidden;
173 +
174 + .os {
175 + line-height: 32px;
176 + height: 32px;
177 + margin-bottom: 4px;
178 + white-space: nowrap;
179 + overflow: hidden;
180 + text-overflow: ellipsis;
181 + }
182 + .ip-address {
183 + white-space: nowrap;
184 + font-size: var(--font-size-0);
185 + font-family: var(--font-mono);
186 + opacity: 0.7;
187 + overflow: hidden;
188 + text-overflow: ellipsis;
189 + }
190 + }
191 +
192 + .agent-actions {
193 + display: flex;
194 +
195 + .box {
196 + padding: var(--size-2) var(--size-2);
197 + background-color: rgba(0, 0, 0, 0.07);
198 + display: flex;
199 + align-items: center;
200 + border-radius: var(--radius-6);
201 + }
202 + }
203 + }
204 +
205 + &:hover {
206 + @extend .card-shadow--medium;
207 + border-color: #e3e8ec;
208 + }
209 +
210 + &.critical {
211 + border-color: $text-color-warning;
212 + }
213 +
214 + @container (max-width: 550px) {
215 + .wrapper {
216 + gap: var(--size-5);
217 +
218 + .agent-header {
219 + min-width: initial;
220 + }
221 + }
222 + }
223 + @container (max-width: 480px) {
224 + .wrapper {
225 + gap: var(--size-4);
226 +
227 + .agent-header {
228 + flex-grow: 1;
229 + overflow: hidden;
230 + }
231 + .agent-info {
232 + display: none;
233 + }
234 + }
235 + }
236 +}
237 +</style>
src/components/agents/AgentToolbar.vue new
+217
@@ -0,0 +1,217 @@
1 +<template>
2 + <div class="agent-toolbar">
3 + <div class="wrapper">
4 + <div class="toolbar-line">
5 + <div class="agents-header">
6 + <h2>Agents</h2>
7 + <el-button @click="emit('sync')" :loading="syncing">
8 + <i class="mdi mdi-account-sync-outline mr-2 fs-18" v-if="!syncing"></i>
9 + <span class="ml-6"> Sync Agents </span>
10 + </el-button>
11 + </div>
12 +
13 + <div class="agent-search">
14 + <el-input :prefix-icon="SearchIcon" placeholder="Search for an agent" clearable v-model="textFilter"> </el-input>
15 +
16 + <div class="search-info">
17 + <strong v-if="agentsFilteredLength !== agentsLength">{{ agentsFilteredLength }}</strong>
18 + <span class="mh-5" v-if="agentsFilteredLength !== agentsLength">/</span>
19 + <strong>{{ agentsLength }}</strong> Agents
20 + </div>
21 + </div>
22 + </div>
23 + <div class="agents-list scrollable only-y">
24 + <div class="agents-critical-list" v-if="agentsCritical.length">
25 + <div class="title">
26 + Critical Assets <small class="o-050">({{ agentsCritical.length }})</small>
27 + </div>
28 + <div class="list">
29 + <div class="item" v-for="agent in agentsCritical" :key="agent.agent_id" @click="emit('click', agent)">
30 + {{ agent.hostname }}
31 + </div>
32 + </div>
33 + </div>
34 + <div class="agents-online-list" v-if="agentsOnline.length">
35 + <div class="title">
36 + Online Agents <small class="o-050">({{ agentsOnline.length }})</small>
37 + </div>
38 + <div class="list">
39 + <div class="item" v-for="agent in agentsOnline" :key="agent.agent_id" @click="emit('click', agent)">
40 + {{ agent.hostname }}
41 + </div>
42 + </div>
43 + </div>
44 + </div>
45 + </div>
46 + </div>
47 +</template>
48 +
49 +<script setup lang="ts">
50 +import { computed, toRefs } from "vue"
51 +import { Agent } from "@/types/agents.d"
52 +import { Search as SearchIcon } from "@element-plus/icons-vue"
53 +
54 +const emit = defineEmits<{
55 + (e: "sync"): void
56 + (e: "update:modelValue", value: string): void
57 + (e: "click", value: Agent): void
58 +}>()
59 +
60 +const props = defineProps<{
61 + modelValue: string
62 + syncing?: boolean
63 + agentsLength?: number
64 + agentsFilteredLength?: number
65 + agentsCritical?: Agent[]
66 + agentsOnline?: Agent[]
67 +}>()
68 +const { modelValue, syncing, agentsLength, agentsFilteredLength, agentsCritical, agentsOnline } = toRefs(props)
69 +
70 +const textFilter = computed<string>({
71 + get() {
72 + return modelValue.value
73 + },
74 + set(value) {
75 + emit("update:modelValue", value)
76 + }
77 +})
78 +</script>
79 +
80 +<style lang="scss" scoped>
81 +@import "@/assets/scss/_variables";
82 +@import "@/assets/scss/card-shadow";
83 +
84 +.agent-toolbar {
85 + container-type: inline-size;
86 + @extend .card-base;
87 + @extend .card-shadow--small;
88 + overflow: hidden;
89 + border: 2px solid transparent;
90 + max-width: 100%;
91 + min-width: 300px;
92 + padding: var(--size-3) var(--size-4);
93 + box-sizing: border-box;
94 + display: flex;
95 + flex-direction: column;
96 +
97 + .wrapper {
98 + display: flex;
99 + flex-direction: column;
100 + gap: var(--size-4);
101 + overflow: hidden;
102 + flex-grow: 1;
103 +
104 + .toolbar-line {
105 + display: flex;
106 + flex-direction: column;
107 + gap: var(--size-4);
108 + overflow: hidden;
109 + }
110 + .agents-header {
111 + display: flex;
112 + align-items: center;
113 + justify-content: space-between;
114 + gap: var(--size-3);
115 +
116 + h2 {
117 + margin: 0;
118 + }
119 + }
120 +
121 + .agent-search {
122 + .search-info {
123 + opacity: 0.5;
124 + text-align: right;
125 + margin-top: var(--size-2);
126 + }
127 + }
128 + .agents-list {
129 + flex-grow: 1;
130 +
131 + .title {
132 + margin-bottom: 6px;
133 + }
134 + .list {
135 + .item {
136 + @extend .card-base;
137 + @extend .card-shadow--small;
138 + border: 2px solid transparent;
139 + padding: var(--size-1) var(--size-2);
140 + font-size: 14px;
141 + font-weight: bold;
142 + cursor: pointer;
143 +
144 + &:not(:last-child) {
145 + margin-bottom: var(--size-2);
146 + }
147 + }
148 + }
149 +
150 + .agents-critical-list {
151 + margin-bottom: var(--size-4);
152 +
153 + .list {
154 + .item {
155 + border-color: $text-color-warning;
156 + }
157 + }
158 + }
159 + .agents-online-list {
160 + .list {
161 + .item {
162 + border-color: $text-color-success;
163 + }
164 + }
165 + }
166 + }
167 + }
168 +
169 + @container (min-width: 350px) {
170 + .wrapper {
171 + gap: var(--size-3);
172 +
173 + .toolbar-line {
174 + flex-direction: row;
175 + align-items: center;
176 + gap: var(--size-3);
177 + }
178 +
179 + .agent-search {
180 + flex-grow: 1;
181 + .search-info {
182 + display: none;
183 + }
184 + }
185 + .agents-list {
186 + display: none;
187 + }
188 + }
189 + }
190 + @media (max-width: 500px) {
191 + .wrapper {
192 + gap: var(--size-3);
193 +
194 + .toolbar-line {
195 + flex-direction: column;
196 + gap: var(--size-3);
197 + }
198 +
199 + .agents-header {
200 + flex-grow: 1;
201 + width: 100%;
202 + }
203 +
204 + .agent-search {
205 + flex-grow: 1;
206 + width: 100%;
207 + .search-info {
208 + display: none;
209 + }
210 + }
211 + .agents-list {
212 + display: none;
213 + }
214 + }
215 + }
216 +}
217 +</style>
src/components/agents/OverviewSection.vue new
+112
@@ -0,0 +1,112 @@
1 +<template>
2 + <div class="overview-section">
3 + <div class="property-group">
4 + <div class="property">
5 + <div class="label">client_id</div>
6 + <div class="value">{{ agent.client_id || "-" }}</div>
7 + </div>
8 + <div class="property">
9 + <div class="label">client_last_seen</div>
10 + <div class="value">{{ formatClientLastSeen || "-" }}</div>
11 + </div>
12 + <div class="property">
13 + <div class="label">ip_address</div>
14 + <div class="value">{{ agent.ip_address || "-" }}</div>
15 + </div>
16 + <div class="property">
17 + <div class="label">label</div>
18 + <div class="value">{{ agent.label || "-" }}</div>
19 + </div>
20 + <div class="property">
21 + <div class="label">last_seen</div>
22 + <div class="value">{{ formatLastSeen || "-" }}</div>
23 + </div>
24 + <div class="property">
25 + <div class="label">os</div>
26 + <div class="value">{{ agent.os || "-" }}</div>
27 + </div>
28 + <div class="property">
29 + <div class="label">velociraptor_client_version</div>
30 + <div class="value">{{ agent.velociraptor_client_version || "-" }}</div>
31 + </div>
32 + <div class="property">
33 + <div class="label">wazuh_agent_version</div>
34 + <div class="value">{{ agent.wazuh_agent_version || "-" }}</div>
35 + </div>
36 + </div>
37 + </div>
38 +</template>
39 +
40 +<script setup lang="ts">
41 +import { computed, toRefs } from "vue"
42 +import dayjs from "dayjs"
43 +import { Agent } from "@/types/agents"
44 +
45 +const props = defineProps<{
46 + agent: Agent
47 +}>()
48 +const { agent } = toRefs(props)
49 +
50 +const formatLastSeen = computed(() => {
51 + const lastSeenDate = dayjs(agent.value.last_seen)
52 + if (!lastSeenDate.isValid()) return agent.value.last_seen
53 +
54 + return lastSeenDate.format("DD/MM/YYYY @ HH:mm")
55 +})
56 +
57 +const formatClientLastSeen = computed(() => {
58 + const lastSeenDate = dayjs(agent.value.client_last_seen)
59 + if (!lastSeenDate.isValid()) return agent.value.last_seen
60 +
61 + return lastSeenDate.format("DD/MM/YYYY @ HH:mm")
62 +})
63 +</script>
64 +
65 +<style lang="scss" scoped>
66 +@import "@/assets/scss/_variables";
67 +
68 +.overview-section {
69 + container-type: inline-size;
70 +
71 + .property-group {
72 + width: 100%;
73 + display: grid;
74 + grid-gap: var(--size-5);
75 + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
76 + grid-auto-flow: row dense;
77 +
78 + .property {
79 + background-color: rgb(244, 244, 244);
80 + padding: var(--size-3);
81 + position: relative;
82 + border-radius: 8px;
83 +
84 + .label {
85 + position: absolute;
86 + top: -8px;
87 + font-size: var(--font-size-0);
88 + background-color: #8d91a1;
89 + padding: 1px 6px;
90 + font-family: var(--font-mono);
91 + border-radius: 5px;
92 + color: white;
93 + max-width: calc(100% - var(--size-8));
94 + overflow: hidden;
95 + white-space: nowrap;
96 + text-overflow: ellipsis;
97 + }
98 +
99 + .value {
100 + position: relative;
101 + top: 5px;
102 + }
103 + }
104 + }
105 +
106 + @container (max-width: 500px) {
107 + .property-group {
108 + grid-template-columns: repeat(auto-fit, 100%);
109 + }
110 + }
111 +}
112 +</style>
src/components/agents/VulnerabilitiesSection.vue new
+80
@@ -0,0 +1,80 @@
1 +<template>
2 + <div class="vulnerabilities-section" v-loading="loading">
3 + <div class="group">
4 + <VulnerabilityCard :vulnerability="item" v-for="item of vulnerabilities" :key="item.id" />
5 + </div>
6 + <div v-if="!loading && !vulnerabilities.length">No vulnerabilities detected</div>
7 + </div>
8 +</template>
9 +
10 +<script setup lang="ts">
11 +import { ref, onBeforeMount, toRefs } from "vue"
12 +import { ElMessage } from "element-plus"
13 +import Api from "@/api"
14 +import { Agent, AgentVulnerabilities } from "@/types/agents"
15 +import VulnerabilityCard from "@/components/agents/VulnerabilityCard.vue"
16 +import { nanoid } from "nanoid"
17 +
18 +const props = defineProps<{
19 + agent: Agent
20 +}>()
21 +const { agent } = toRefs(props)
22 +
23 +const loading = ref(false)
24 +const vulnerabilities = ref<AgentVulnerabilities[]>([])
25 +
26 +function getVulnerabilities(id: string) {
27 + loading.value = true
28 +
29 + Api.agents
30 + .agentVulnerabilities(id)
31 + .then(res => {
32 + if (res.data.success) {
33 + vulnerabilities.value = (res.data.vulnerabilities || []).map(o => {
34 + o.id = nanoid()
35 + return o
36 + })
37 + } else {
38 + ElMessage({
39 + message: res.data?.message || "An error occurred. Please try again later.",
40 + type: "warning"
41 + })
42 + }
43 + })
44 + .catch(err => {
45 + ElMessage({
46 + message: err.response?.data?.message || "An error occurred. Please try again later.",
47 + type: "error"
48 + })
49 + })
50 + .finally(() => {
51 + loading.value = false
52 + })
53 +}
54 +
55 +onBeforeMount(() => {
56 + if (agent?.value?.agent_id) getVulnerabilities(agent.value.agent_id)
57 +})
58 +</script>
59 +
60 +<style lang="scss" scoped>
61 +@import "@/assets/scss/_variables";
62 +
63 +.vulnerabilities-section {
64 + container-type: inline-size;
65 +
66 + .group {
67 + width: 100%;
68 + display: grid;
69 + grid-gap: var(--size-5);
70 + grid-template-columns: repeat(auto-fit, minmax(175px, 1fr));
71 + grid-auto-flow: row dense;
72 + }
73 +
74 + @container (max-width: 500px) {
75 + .group {
76 + grid-template-columns: repeat(auto-fit, 100%);
77 + }
78 + }
79 +}
80 +</style>
src/components/agents/VulnerabilityCard.vue new
+196
@@ -0,0 +1,196 @@
1 +<template>
2 + <div class="vulnerability-card" :class="`severity-${vulnerability.severity}`" @click="showDialog = true">
3 + <div class="severity">{{ vulnerability.severity }}</div>
4 + <div class="wrapper">
5 + <div class="title">
6 + <span>{{ vulnerability.title }}</span>
7 + </div>
8 + <div class="property" aria-label="Detection time" data-balloon-pos="up" data-balloon-length="medium">
9 + <i class="mdi mdi-clock-outline"></i>
10 + <span>{{ detectionTime }}</span>
11 + </div>
12 + <div
13 + class="property"
14 + :aria-label="`CVSS2: ${vulnerability.cvss2_score} - CVSS3: ${vulnerability.cvss3_score}`"
15 + data-balloon-pos="up"
16 + data-balloon-length="medium"
17 + >
18 + <i class="mdi mdi-counter"></i>
19 + <span>{{ vulnerability.cve }}</span>
20 + </div>
21 + <div class="property" :aria-label="`Version: ${vulnerability.version}`" data-balloon-pos="up" data-balloon-length="medium">
22 + <i class="mdi mdi-crosshairs-question"></i>
23 + <span>{{ vulnerability.name }}</span>
24 + </div>
25 + </div>
26 + </div>
27 + <el-dialog :show-close="true" class="vulnerability-dialog" v-model="showDialog" width="90%" append-to-body>
28 + <div class="vulnerability-property-group" v-if="vulnerabilitySanitized">
29 + <div class="prop" v-for="item of vulnerabilitySanitized" :key="item.label">
30 + <div class="value">{{ item.value ?? "-" }}</div>
31 + <div class="label">{{ item.label }}</div>
32 + </div>
33 + </div>
34 + <div class="vulnerability-references">
35 + <div class="title">External references</div>
36 + <ul class="list">
37 + <li v-for="ref of vulnerability.external_references" :key="ref">
38 + <a :href="ref" target="_blank">{{ ref }}</a>
39 + </li>
40 + </ul>
41 + </div>
42 + </el-dialog>
43 +</template>
44 +
45 +<script setup lang="ts">
46 +import { computed, ref, toRefs } from "vue"
47 +import { AgentVulnerabilities } from "@/types/agents.d"
48 +import dayjs from "dayjs"
49 +import { cloneDeep } from "lodash"
50 +
51 +const props = defineProps<{
52 + vulnerability: AgentVulnerabilities
53 +}>()
54 +const { vulnerability } = toRefs(props)
55 +
56 +const vulnerabilitySanitized = computed(() => {
57 + const newObj = []
58 + const obj = cloneDeep(vulnerability.value)
59 + for (const k in obj) {
60 + if (typeof obj[k] === "string") {
61 + const maybeTime = dayjs(obj[k])
62 + if (maybeTime.isValid()) {
63 + obj[k] = maybeTime.format("DD/MM/YYYY @ HH:mm")
64 + }
65 + }
66 +
67 + if (!["external_references", "id"].includes(k))
68 + newObj.push({
69 + label: k,
70 + value: obj[k]
71 + })
72 + }
73 + return newObj
74 +})
75 +
76 +const detectionTime = computed(() => {
77 + const detection_time = dayjs(vulnerability.value.detection_time)
78 + if (!detection_time.isValid()) return vulnerability.value.detection_time
79 +
80 + return detection_time.format("DD/MM/YYYY @ HH:mm")
81 +})
82 +
83 +const showDialog = ref(false)
84 +</script>
85 +
86 +<style lang="scss" scoped>
87 +@import "@/assets/scss/_variables";
88 +@import "@/assets/scss/card-shadow";
89 +
90 +.vulnerability-card {
91 + @extend .card-base;
92 + overflow: visible;
93 + border: 2px solid #e3e8ec;
94 + max-width: 100%;
95 + box-sizing: border-box;
96 + transition: all 0.3s;
97 + cursor: pointer;
98 + position: relative;
99 +
100 + .severity {
101 + position: absolute;
102 + top: -9px;
103 + left: var(--size-2);
104 + color: white;
105 + border-radius: 6px;
106 + line-height: 1;
107 + background-color: #c4d0dc;
108 + text-transform: uppercase;
109 + font-family: var(--font-mono);
110 + font-size: var(--font-size-0);
111 + white-space: nowrap;
112 + overflow: hidden;
113 + text-overflow: ellipsis;
114 + padding: 3px var(--size-2);
115 + }
116 +
117 + .wrapper {
118 + padding: var(--size-3) calc(var(--size-2) + 2px);
119 +
120 + .title {
121 + font-weight: bold;
122 + line-height: 1.2;
123 + margin-bottom: 10px;
124 + }
125 +
126 + .property {
127 + font-size: 13px;
128 + margin-bottom: 2px;
129 +
130 + i {
131 + margin-right: 5px;
132 + }
133 + }
134 + }
135 +
136 + &.severity-Critical {
137 + border-color: $text-color-danger;
138 + .severity {
139 + background-color: $text-color-danger;
140 + }
141 + }
142 + &.severity-High {
143 + border-color: $text-color-danger;
144 + .severity {
145 + background-color: $text-color-danger;
146 + }
147 + }
148 + &.severity-Low {
149 + }
150 + &.severity-Medium {
151 + border-color: $text-color-warning;
152 + .severity {
153 + background-color: $text-color-warning;
154 + }
155 + }
156 + &.severity-Untriaged {
157 + }
158 +}
159 +
160 +.vulnerability-property-group {
161 + width: 100%;
162 + display: grid;
163 + box-sizing: border-box;
164 + padding: var(--size-2) var(--size-4);
165 + grid-gap: var(--size-5);
166 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
167 + grid-auto-flow: row dense;
168 +
169 + .prop {
170 + .value {
171 + font-weight: bold;
172 + margin-bottom: 2px;
173 + white-space: nowrap;
174 + }
175 + .label {
176 + font-size: var(--font-size-0);
177 + font-family: var(--font-mono);
178 + opacity: 0.8;
179 + }
180 + }
181 +}
182 +
183 +.vulnerability-references {
184 + padding: 0 var(--size-4);
185 + padding-top: var(--size-4);
186 + overflow: hidden;
187 +
188 + .list {
189 + padding-left: 16px;
190 +
191 + li {
192 + word-break: break-all;
193 + }
194 + }
195 +}
196 +</style>
src/components/agents/utils.ts new
+155
@@ -0,0 +1,155 @@
1 +import dayjs from "dayjs"
2 +import Api from "@/api"
3 +import { ElMessage, ElMessageBox } from "element-plus"
4 +import { Agent } from "@/types/agents"
5 +
6 +export function isAgentOnline(last_seen: string) {
7 + const lastSeenDate = dayjs(last_seen)
8 + if (!lastSeenDate.isValid()) return false
9 +
10 + return lastSeenDate.isAfter(dayjs().subtract(1, "h"))
11 +}
12 +
13 +export interface ToggleAgentCriticalParams {
14 + agentId: string
15 + criticalStatus: boolean
16 + cbBefore?: () => void
17 + cbSuccess?: () => void
18 + cbAfter?: () => void
19 + cbError?: () => void
20 +}
21 +
22 +export function toggleAgentCritical({ agentId, criticalStatus, cbBefore, cbSuccess, cbAfter, cbError }: ToggleAgentCriticalParams) {
23 + if (cbBefore && typeof cbBefore === "function") {
24 + cbBefore()
25 + }
26 + const method = criticalStatus ? "markNonCritical" : "markCritical"
27 +
28 + Api.agents[method](agentId)
29 + .then(res => {
30 + if (res.data.success) {
31 + ElMessage({
32 + message: "Agent Criticality Updated Successfully",
33 + type: "success"
34 + })
35 +
36 + if (cbSuccess && typeof cbSuccess === "function") {
37 + cbSuccess()
38 + }
39 + } else {
40 + ElMessage({
41 + message: res.data?.message || "Failed to Update Agent Criticality.",
42 + type: "error"
43 + })
44 +
45 + if (cbError && typeof cbError === "function") {
46 + cbError()
47 + }
48 + }
49 + })
50 + .catch(err => {
51 + if (err.response.status === 401) {
52 + ElMessage({
53 + message: err.response?.data?.message || "Agent Criticality Update returned Unauthorized.",
54 + type: "error"
55 + })
56 + } else {
57 + ElMessage({
58 + message: err.response?.data?.message || "Failed to Update Agent Criticality",
59 + type: "error"
60 + })
61 + }
62 +
63 + if (cbError && typeof cbError === "function") {
64 + cbError()
65 + }
66 + })
67 + .finally(() => {
68 + if (cbAfter && typeof cbAfter === "function") {
69 + cbAfter()
70 + }
71 + })
72 +}
73 +
74 +export interface DeleteAgentParams {
75 + agent: Agent
76 + cbBefore?: () => void
77 + cbSuccess?: () => void
78 + cbAfter?: () => void
79 + cbError?: () => void
80 +}
81 +
82 +export function handleDeleteAgent({ agent, cbBefore, cbSuccess, cbAfter, cbError }: DeleteAgentParams) {
83 + ElMessageBox.confirm(`Are you sure you want to delete the agent:<br/><strong>${agent.hostname}</strong> ?`, "Warning", {
84 + confirmButtonText: "Yes I'm sure",
85 + confirmButtonClass: "el-button--warning",
86 + cancelButtonText: "Cancel",
87 + type: "warning",
88 + dangerouslyUseHTMLString: true,
89 + customStyle: {
90 + width: "90%",
91 + maxWidth: "400px"
92 + }
93 + })
94 + .then(() => {
95 + deleteAgent({ agent, cbBefore, cbSuccess, cbAfter, cbError })
96 + })
97 + .catch(() => {
98 + ElMessage({
99 + type: "info",
100 + message: "Delete canceled"
101 + })
102 + })
103 +}
104 +
105 +export function deleteAgent({ agent, cbBefore, cbSuccess, cbAfter, cbError }: DeleteAgentParams) {
106 + if (cbBefore && typeof cbBefore === "function") {
107 + cbBefore()
108 + }
109 +
110 + Api.agents
111 + .deleteAgent(agent.agent_id)
112 + .then(res => {
113 + if (res.data.success) {
114 + ElMessage({
115 + message: "Agent was successfully deleted.",
116 + type: "success"
117 + })
118 +
119 + if (cbSuccess && typeof cbSuccess === "function") {
120 + cbSuccess()
121 + }
122 + } else {
123 + ElMessage({
124 + message: res.data?.message || "An error occurred. Please try again later.",
125 + type: "error"
126 + })
127 +
128 + if (cbError && typeof cbError === "function") {
129 + cbError()
130 + }
131 + }
132 + })
133 + .catch(err => {
134 + if (err.response.status === 401) {
135 + ElMessage({
136 + message: err.response?.data?.message || "Agent Delete returned Unauthorized.",
137 + type: "error"
138 + })
139 + } else {
140 + ElMessage({
141 + message: err.response?.data?.message || "An error occurred. Please try again later.",
142 + type: "error"
143 + })
144 + }
145 +
146 + if (cbError && typeof cbError === "function") {
147 + cbError()
148 + }
149 + })
150 + .finally(() => {
151 + if (cbAfter && typeof cbAfter === "function") {
152 + cbAfter()
153 + }
154 + })
155 +}
src/components/indices/ClusterHealth.vue
+1
@@ -109,6 +109,7 @@ onBeforeMount(() => {
109 .cluster-health {
110 padding: var(--size-5) var(--size-6);
111 @extend .card-base;
112 + @extend .card-shadow--small;
113
114 .title {
115 font-size: var(--font-size-4);
src/components/indices/Marquee.vue
+2 -1
@@ -50,13 +50,14 @@ const loading = computed(() => !indices?.value || indices.value === null)
50 .indices-marquee {
51 .info {
52 opacity: 0.5;
53 - font-size: 12px;
53 + font-size: var(--font-size-0);
54 margin-top: 5px;
55 }
56 .marquee-wrap {
57 height: 45px;
58 transform: translate3d(0, 0, 0);
59 @extend .card-base;
60 + @extend .card-shadow--small;
61
62 :deep() {
63 .marquee {
src/components/indices/NodeAllocation.vue
+4
@@ -121,6 +121,7 @@ onBeforeMount(() => {
121 .cluster-health {
122 padding: var(--size-5) var(--size-6);
123 @extend .card-base;
124 + @extend .card-shadow--small;
125
126 .title {
127 font-size: var(--font-size-4);
@@ -129,12 +130,15 @@ onBeforeMount(() => {
130 }
131 .info {
132 min-height: 50px;
133 + margin-left: -5px;
134 + margin-right: -5px;
135
136 .item {
137 padding: var(--size-3) var(--size-4);
138 @extend .card-base;
139 @extend .card-shadow--small;
140 border: 2px solid transparent;
141 + margin: 5px;
142
143 display: flex;
144 flex-direction: column;
src/components/indices/TopIndices.vue
+1 -1
@@ -1,7 +1,7 @@
1 <template>
2 <div class="top-indices-chart-container">
3 <div class="title">Top 8 indices size & health</div>
4 - <div style="height: 400px" v-loading="loading">
4 + <div style="height: 400px; overflow: hidden" v-loading="loading">
5 <div id="top-indices-chart" style="max-width: 100%; height: 400px"></div>
6 </div>
7 </div>
src/components/indices/UnhealthyIndices.vue
+1
@@ -50,6 +50,7 @@ const unhealthyIndices = computed(() =>
50 .unhealthy-indices {
51 padding: var(--size-5) var(--size-6);
52 @extend .card-base;
53 + @extend .card-shadow--small;
54
55 .title {
56 font-size: var(--font-size-4);
src/components/inputs/Details.vue new
+125
@@ -0,0 +1,125 @@
1 +<template>
2 + <div class="input-details-box" v-loading="loading" :class="{ active: currentInput }">
3 + <div class="box-header">
4 + <div class="title">
5 + <span v-if="currentInput"> Below the details for input </span>
6 + <span v-else> Select an input to see the details </span>
7 + </div>
8 + <div class="select-box" v-if="inputs && inputs.length">
9 + <el-select v-model="currentInput" placeholder="Inputs list" clearable value-key="input" filterable>
10 + <el-option v-for="input in inputs" :key="input.id" :label="input.title" :value="input"></el-option>
11 + </el-select>
12 + </div>
13 + </div>
14 + <div class="details-box" v-if="currentInput">
15 + <div class="info">
16 + <InputCard :input="currentInput" showActions @delete="clearCurrentInput()" />
17 + </div>
18 + </div>
19 + </div>
20 +</template>
21 +
22 +<script setup lang="ts">
23 +import { computed, onBeforeMount, ref, toRefs } from "vue"
24 +import { Inputs } from "@/types/graylog.d"
25 +import { ElMessage } from "element-plus"
26 +import InputCard from "@/components/inputs/InputCard.vue"
27 +import Api from "@/api"
28 +import { nanoid } from "nanoid"
29 +
30 +type InputModel = Inputs | null | ""
31 +
32 +const emit = defineEmits<{
33 + (e: "update:modelValue", value: InputModel): void
34 +}>()
35 +
36 +const props = defineProps<{
37 + inputs: Inputs[] | null
38 + modelValue: InputModel
39 +}>()
40 +const { inputs, modelValue } = toRefs(props)
41 +
42 +const loading = computed(() => !inputs?.value || inputs.value === null)
43 +
44 +const currentInput = computed<InputModel>({
45 + get() {
46 + return modelValue.value
47 + },
48 + set(value) {
49 + emit("update:modelValue", value)
50 + }
51 +})
52 +
53 +function clearCurrentInput() {
54 + currentInput.value = null
55 +}
56 +
57 +onBeforeMount(() => {
58 + // getShards()
59 +})
60 +</script>
61 +
62 +<style lang="scss" scoped>
63 +@import "@/assets/scss/_variables";
64 +@import "@/assets/scss/card-shadow";
65 +
66 +.input-details-box {
67 + padding: var(--size-5) var(--size-6);
68 + border: 2px solid transparent;
69 + @extend .card-base;
70 + &.active {
71 + border-color: $text-color-accent;
72 + @extend .card-shadow--small;
73 + }
74 +
75 + .box-header {
76 + display: flex;
77 + align-items: center;
78 +
79 + .title {
80 + margin-right: var(--size-4);
81 + }
82 +
83 + .select-box {
84 + .el-select {
85 + min-width: var(--size-fluid-9);
86 + max-width: 100%;
87 + }
88 + }
89 + }
90 +
91 + .details-box {
92 + margin-top: var(--size-6);
93 +
94 + .shards {
95 + margin-top: var(--size-4);
96 + @extend .card-base;
97 + @extend .card-shadow--small;
98 +
99 + .shard-state {
100 + font-weight: bold;
101 + &.STARTED {
102 + color: $text-color-success;
103 + }
104 + &.UNASSIGNED {
105 + color: $text-color-warning;
106 + }
107 + }
108 + }
109 + }
110 +
111 + @media (max-width: 1000px) {
112 + .box-header {
113 + flex-direction: column;
114 + align-items: flex-start;
115 + gap: var(--size-2);
116 + .select-box {
117 + width: 100%;
118 + .el-select {
119 + min-width: 100%;
120 + }
121 + }
122 + }
123 + }
124 +}
125 +</style>
src/components/inputs/InputCard.vue new
+265
@@ -0,0 +1,265 @@
1 +<template>
2 + <div class="input-card" :class="[`health-green`]" v-loading="loading">
3 + <div class="group">
4 + <div class="box">
5 + <div class="value">{{ input.title }}</div>
6 + <div class="label">name</div>
7 + </div>
8 + <div class="box">
9 + <div class="value">{{ input.id }}</div>
10 + <div class="label">id</div>
11 + </div>
12 + <!-- <div class="box">
13 + <div class="value text-uppercase">
14 + <InputIcon :health="input.health" color />
15 + {{ input.health }}
16 + </div>
17 + <div class="label">health</div>
18 + </div> -->
19 + </div>
20 + <div class="group">
21 + <div class="box">
22 + <div class="value">{{ input.port }}</div>
23 + <div class="label">port</div>
24 + </div>
25 + <div class="box">
26 + <div class="value">{{ input.inputstate }}</div>
27 + <div class="label">state</div>
28 + </div>
29 + </div>
30 + <div class="group actions" v-if="showActions">
31 + <div class="box">
32 + <!--
33 + <el-tooltip content="Rotate" placement="top" :show-arrow="false">
34 + <el-button type="primary" :icon="RefreshIcon" circle />
35 + </el-tooltip>
36 + -->
37 + <el-tooltip content="Start Input" placement="top" :show-arrow="false">
38 + <el-button type="primary" :icon="DeleteIcon" circle @click="handleStart" />
39 + </el-tooltip>
40 + <el-tooltip content="Stop Input" placement="top" :show-arrow="false">
41 + <el-button type="danger" :icon="DeleteIcon" circle @click="handleStop" />
42 + </el-tooltip>
43 + </div>
44 + </div>
45 + </div>
46 +</template>
47 +
48 +<!-- arrow-down-drop-circle
49 +"mdi mdi-arrow-down-drop-circle" -->
50 +
51 +<script setup lang="ts">
52 +import { ref, toRefs } from "vue"
53 +import InputIcon from "@/components/inputs/InputIcon.vue"
54 +import { Inputs } from "@/types/graylog.d"
55 +import Api from "@/api"
56 +import { ElMessage, ElMessageBox } from "element-plus"
57 +import { Refresh as RefreshIcon, Delete as DeleteIcon } from "@element-plus/icons-vue"
58 +
59 +const emit = defineEmits<{
60 + (e: "delete"): void
61 +}>()
62 +
63 +const props = defineProps<{
64 + input: Inputs
65 + showActions?: boolean
66 +}>()
67 +const { input, showActions } = toRefs(props)
68 +
69 +const loading = ref(false)
70 +
71 +const handleStop = () => {
72 + ElMessageBox.confirm(`Are you sure you want to stop the Input:<br/><strong>${input.value.title}</strong> ?`, "Warning", {
73 + confirmButtonText: "Yes I'm sure",
74 + confirmButtonClass: "el-button--warning",
75 + cancelButtonText: "Cancel",
76 + type: "warning",
77 + dangerouslyUseHTMLString: true,
78 + customStyle: {
79 + width: "90%",
80 + maxWidth: "400px"
81 + }
82 + })
83 + .then(() => {
84 + stopInput()
85 + })
86 + .catch(() => {
87 + ElMessage({
88 + type: "info",
89 + message: "Stop canceled"
90 + })
91 + })
92 +}
93 +
94 +function stopInput() {
95 + loading.value = true
96 +
97 + Api.graylog
98 + .stopInput(input.value.id)
99 + .then(res => {
100 + if (res.data.success) {
101 + ElMessage({
102 + message: "Input was successfully stopped.",
103 + type: "success"
104 + })
105 +
106 + emit("delete")
107 + } else {
108 + ElMessage({
109 + message: res.data?.message || "An error occurred. Please try again later.",
110 + type: "error"
111 + })
112 + }
113 + })
114 + .catch(err => {
115 + if (err.response.status === 401) {
116 + ElMessage({
117 + message: err.response?.data?.message || "Graylog returned Unauthorized. Please check your connector credentials.",
118 + type: "error"
119 + })
120 + } else if (err.response.status === 404) {
121 + ElMessage({
122 + message: err.response?.data?.message || "An error occurred. Please try again later.",
123 + type: "error"
124 + })
125 + } else {
126 + ElMessage({
127 + message: err.response?.data?.message || "An error occurred. Please try again later.",
128 + type: "error"
129 + })
130 + }
131 + })
132 + .finally(() => {
133 + loading.value = false
134 + })
135 +}
136 +
137 +const handleStart = () => {
138 + ElMessageBox.confirm(`Are you sure you want to start the Input:<br/><strong>${input.value.title}</strong> ?`, "Warning", {
139 + confirmButtonText: "Yes I'm sure",
140 + confirmButtonClass: "el-button--warning",
141 + cancelButtonText: "Cancel",
142 + type: "warning",
143 + dangerouslyUseHTMLString: true,
144 + customStyle: {
145 + width: "90%",
146 + maxWidth: "400px"
147 + }
148 + })
149 + .then(() => {
150 + startInput()
151 + })
152 + .catch(() => {
153 + ElMessage({
154 + type: "info",
155 + message: "Stop canceled"
156 + })
157 + })
158 +}
159 +
160 +function startInput() {
161 + loading.value = true
162 +
163 + Api.graylog
164 + .startInput(input.value.id)
165 + .then(res => {
166 + if (res.data.success) {
167 + ElMessage({
168 + message: "Input was successfully started.",
169 + type: "success"
170 + })
171 +
172 + emit("delete")
173 + } else {
174 + ElMessage({
175 + message: res.data?.message || "An error occurred. Please try again later.",
176 + type: "error"
177 + })
178 + }
179 + })
180 + .catch(err => {
181 + if (err.response.status === 401) {
182 + ElMessage({
183 + message: err.response?.data?.message || "Graylog returned Unauthorized. Please check your connector credentials.",
184 + type: "error"
185 + })
186 + } else if (err.response.status === 404) {
187 + ElMessage({
188 + message: err.response?.data?.message || "An error occurred. Please try again later.",
189 + type: "error"
190 + })
191 + } else {
192 + ElMessage({
193 + message: err.response?.data?.message || "An error occurred. Please try again later.",
194 + type: "error"
195 + })
196 + }
197 + })
198 + .finally(() => {
199 + loading.value = false
200 + })
201 +}
202 +</script>
203 +
204 +<style lang="scss" scoped>
205 +@import "@/assets/scss/_variables";
206 +@import "@/assets/scss/card-shadow";
207 +
208 +.input-card {
209 + padding: var(--size-3) var(--size-4);
210 + @extend .card-base;
211 + @extend .card-shadow--small;
212 + border: 2px solid transparent;
213 +
214 + display: flex;
215 + justify-content: space-between;
216 + gap: var(--size-6);
217 + flex-wrap: wrap;
218 +
219 + .group {
220 + display: flex;
221 + justify-content: space-between;
222 + gap: var(--size-6);
223 + flex-grow: 1;
224 + flex-wrap: wrap;
225 +
226 + .box {
227 + flex-grow: 1;
228 +
229 + .value {
230 + font-weight: bold;
231 + margin-bottom: 2px;
232 + white-space: nowrap;
233 + }
234 + .label {
235 + white-space: nowrap;
236 + font-size: var(--font-size-0);
237 + font-family: var(--font-mono);
238 + opacity: 0.8;
239 + }
240 + }
241 + &.actions {
242 + flex-grow: 0;
243 + .box {
244 + padding: var(--size-2) var(--size-2);
245 + background-color: rgba(0, 0, 0, 0.07);
246 + display: flex;
247 + align-items: center;
248 + border-radius: var(--radius-6);
249 + }
250 + }
251 + }
252 +
253 + &.health-green {
254 + border-color: $text-color-success;
255 + }
256 +
257 + &.health-yellow {
258 + border-color: $text-color-warning;
259 + }
260 +
261 + &.health-red {
262 + border-color: $text-color-danger;
263 + }
264 +}
265 +</style>
src/components/inputs/InputIcon.vue new
+29
@@ -0,0 +1,29 @@
1 +<template>
2 + <span class="input-icon" :class="[`state-${state}`, { color }]">
3 + <i v-if="state === InputState.RUNNING" class="mdi mdi-shield-check"></i>
4 + </span>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import { toRefs } from "vue"
9 +import { RunningInput, InputState } from "@/types/graylog.d"
10 +
11 +const props = defineProps<{
12 + state: RunningInput["state"]
13 + color?: boolean
14 +}>()
15 +const { state, color } = toRefs(props)
16 +</script>
17 +
18 +<style lang="scss" scoped>
19 +@import "@/assets/scss/_variables";
20 +@import "@/assets/scss/card-shadow";
21 +
22 +.input-icon {
23 + &.color {
24 + &.state-RUNNING {
25 + color: $text-color-success;
26 + }
27 + }
28 +}
29 +</style>
src/components/inputs/Marquee.vue new
+104
@@ -0,0 +1,104 @@
1 +<template>
2 + <div class="inputs-marquee" v-loading="loading">
3 + <Vue3Marquee
4 + class="marquee-wrap"
5 + :duration="200"
6 + :pauseOnHover="true"
7 + :clone="true"
8 + :gradient="true"
9 + :gradient-color="[255, 255, 255]"
10 + gradient-length="10%"
11 + >
12 + <span
13 + v-for="item in parsedItems"
14 + :key="item.id"
15 + class="item"
16 + :class="item.state"
17 + @click="emit('click', item)"
18 + title="Click to select"
19 + >
20 + <InputIcon :state="item.state" color />
21 + {{ item.title }}
22 + </span>
23 + </Vue3Marquee>
24 + <div class="info"><i class="mdi mdi-information-outline"></i> Click on an input to select</div>
25 + </div>
26 +</template>
27 +
28 +<script setup lang="ts">
29 +import { computed, toRefs } from "vue"
30 +import { RunningInput } from "@/types/graylog.d"
31 +import { Vue3Marquee } from "vue3-marquee"
32 +import InputIcon from "@/components/inputs/InputIcon.vue"
33 +
34 +const MIN_ITEMS = 8
35 +
36 +const emit = defineEmits<{
37 + (e: "click", value: RunningInput): void
38 +}>()
39 +
40 +const props = defineProps<{
41 + inputs: RunningInput[] | null
42 +}>()
43 +
44 +const { inputs } = toRefs(props)
45 +
46 +const parsedItems = computed(() => {
47 + if (!inputs.value) {
48 + return []
49 + }
50 +
51 + if (inputs.value.length >= MIN_ITEMS) {
52 + return inputs.value
53 + }
54 +
55 + const list = []
56 + while (list.length < MIN_ITEMS) {
57 + list.push(...inputs.value)
58 + }
59 +
60 + return list
61 +})
62 +
63 +const loading = computed(() => !inputs?.value || inputs.value === null)
64 +</script>
65 +
66 +<style lang="scss" scoped>
67 +@import "@/assets/scss/_variables";
68 +@import "@/assets/scss/card-shadow";
69 +
70 +.inputs-marquee {
71 + .info {
72 + opacity: 0.5;
73 + font-size: 12px;
74 + margin-top: 5px;
75 + }
76 + .marquee-wrap {
77 + height: 45px;
78 + transform: translate3d(0, 0, 0);
79 + @extend .card-base;
80 +
81 + :deep() {
82 + .marquee {
83 + transform: translate3d(0, 0, 0);
84 + }
85 + .overlay {
86 + &:after {
87 + right: -1px;
88 + }
89 + }
90 + }
91 +
92 + .item {
93 + padding: 10px 20px;
94 + cursor: pointer;
95 +
96 + &.RUNNING {
97 + i {
98 + color: $text-color-success;
99 + }
100 + }
101 + }
102 + }
103 +}
104 +</style>
src/components/inputs/StreamCard.vue new
+269
@@ -0,0 +1,269 @@
1 +<template>
2 + <div class="stream-card" :class="[`health-green`]" v-loading="loading">
3 + <div class="group">
4 + <div class="box">
5 + <div class="value">{{ stream.title }}</div>
6 + <div class="label">name</div>
7 + </div>
8 + <div class="box">
9 + <div class="value">{{ stream.description }}</div>
10 + <div class="label">description</div>
11 + </div>
12 + <div class="box">
13 + <div class="value">{{ stream.id }}</div>
14 + <div class="label">id</div>
15 + </div>
16 + <!-- <div class="box">
17 + <div class="value text-uppercase">
18 + <InputIcon :health="input.health" color />
19 + {{ input.health }}
20 + </div>
21 + <div class="label">health</div>
22 + </div> -->
23 + </div>
24 + <div class="group">
25 + <div class="box">
26 + <div class="value">{{ stream.disabled }}</div>
27 + <div class="label">disabled</div>
28 + </div>
29 + <div class="box">
30 + <div class="value">{{ stream.rules.length }}</div>
31 + <div class="label">number of assigned rules</div>
32 + </div>
33 + </div>
34 + <div class="group actions" v-if="showActions">
35 + <div class="box">
36 + <!--
37 + <el-tooltip content="Rotate" placement="top" :show-arrow="false">
38 + <el-button type="primary" :icon="RefreshIcon" circle />
39 + </el-tooltip>
40 + -->
41 + <el-tooltip content="Start Stream" placement="top" :show-arrow="false">
42 + <el-button type="primary" :icon="DeleteIcon" circle @click="handleStart" />
43 + </el-tooltip>
44 + <el-tooltip content="Stop Stream" placement="top" :show-arrow="false">
45 + <el-button type="danger" :icon="DeleteIcon" circle @click="handleStop" />
46 + </el-tooltip>
47 + </div>
48 + </div>
49 + </div>
50 +</template>
51 +
52 +<!-- arrow-down-drop-circle
53 +"mdi mdi-arrow-down-drop-circle" -->
54 +
55 +<script setup lang="ts">
56 +import { ref, toRefs } from "vue"
57 +import StreamIcon from "@/components/inputs/InputIcon.vue"
58 +import { Streams } from "@/types/graylog.d"
59 +import Api from "@/api"
60 +import { ElMessage, ElMessageBox } from "element-plus"
61 +import { Refresh as RefreshIcon, Delete as DeleteIcon } from "@element-plus/icons-vue"
62 +
63 +const emit = defineEmits<{
64 + (e: "delete"): void
65 +}>()
66 +
67 +const props = defineProps<{
68 + stream: Streams
69 + showActions?: boolean
70 +}>()
71 +const { stream, showActions } = toRefs(props)
72 +
73 +const loading = ref(false)
74 +
75 +const handleStop = () => {
76 + ElMessageBox.confirm(`Are you sure you want to stop the Stream:<br/><strong>${stream.value.title}</strong> ?`, "Warning", {
77 + confirmButtonText: "Yes I'm sure",
78 + confirmButtonClass: "el-button--warning",
79 + cancelButtonText: "Cancel",
80 + type: "warning",
81 + dangerouslyUseHTMLString: true,
82 + customStyle: {
83 + width: "90%",
84 + maxWidth: "400px"
85 + }
86 + })
87 + .then(() => {
88 + stopStream()
89 + })
90 + .catch(() => {
91 + ElMessage({
92 + type: "info",
93 + message: "Stop canceled"
94 + })
95 + })
96 +}
97 +
98 +function stopStream() {
99 + loading.value = true
100 +
101 + Api.graylog
102 + .stopStream(stream.value.id)
103 + .then(res => {
104 + if (res.data.success) {
105 + ElMessage({
106 + message: "Stream was successfully stopped.",
107 + type: "success"
108 + })
109 +
110 + emit("delete")
111 + } else {
112 + ElMessage({
113 + message: res.data?.message || "An error occurred. Please try again later.",
114 + type: "error"
115 + })
116 + }
117 + })
118 + .catch(err => {
119 + if (err.response.status === 401) {
120 + ElMessage({
121 + message: err.response?.data?.message || "Graylog returned Unauthorized. Please check your connector credentials.",
122 + type: "error"
123 + })
124 + } else if (err.response.status === 404) {
125 + ElMessage({
126 + message: err.response?.data?.message || "An error occurred. Please try again later.",
127 + type: "error"
128 + })
129 + } else {
130 + ElMessage({
131 + message: err.response?.data?.message || "An error occurred. Please try again later.",
132 + type: "error"
133 + })
134 + }
135 + })
136 + .finally(() => {
137 + loading.value = false
138 + })
139 +}
140 +
141 +const handleStart = () => {
142 + ElMessageBox.confirm(`Are you sure you want to start the Stream:<br/><strong>${stream.value.title}</strong> ?`, "Warning", {
143 + confirmButtonText: "Yes I'm sure",
144 + confirmButtonClass: "el-button--warning",
145 + cancelButtonText: "Cancel",
146 + type: "warning",
147 + dangerouslyUseHTMLString: true,
148 + customStyle: {
149 + width: "90%",
150 + maxWidth: "400px"
151 + }
152 + })
153 + .then(() => {
154 + startStream()
155 + })
156 + .catch(() => {
157 + ElMessage({
158 + type: "info",
159 + message: "Stop canceled"
160 + })
161 + })
162 +}
163 +
164 +function startStream() {
165 + loading.value = true
166 +
167 + Api.graylog
168 + .startStream(stream.value.id)
169 + .then(res => {
170 + if (res.data.success) {
171 + ElMessage({
172 + message: "Stream was successfully started.",
173 + type: "success"
174 + })
175 +
176 + emit("delete")
177 + } else {
178 + ElMessage({
179 + message: res.data?.message || "An error occurred. Please try again later.",
180 + type: "error"
181 + })
182 + }
183 + })
184 + .catch(err => {
185 + if (err.response.status === 401) {
186 + ElMessage({
187 + message: err.response?.data?.message || "Graylog returned Unauthorized. Please check your connector credentials.",
188 + type: "error"
189 + })
190 + } else if (err.response.status === 404) {
191 + ElMessage({
192 + message: err.response?.data?.message || "An error occurred. Please try again later.",
193 + type: "error"
194 + })
195 + } else {
196 + ElMessage({
197 + message: err.response?.data?.message || "An error occurred. Please try again later.",
198 + type: "error"
199 + })
200 + }
201 + })
202 + .finally(() => {
203 + loading.value = false
204 + })
205 +}
206 +</script>
207 +
208 +<style lang="scss" scoped>
209 +@import "@/assets/scss/_variables";
210 +@import "@/assets/scss/card-shadow";
211 +
212 +.stream-card {
213 + padding: var(--size-3) var(--size-4);
214 + @extend .card-base;
215 + @extend .card-shadow--small;
216 + border: 2px solid transparent;
217 +
218 + display: flex;
219 + justify-content: space-between;
220 + gap: var(--size-6);
221 + flex-wrap: wrap;
222 +
223 + .group {
224 + display: flex;
225 + justify-content: space-between;
226 + gap: var(--size-6);
227 + flex-grow: 1;
228 + flex-wrap: wrap;
229 +
230 + .box {
231 + flex-grow: 1;
232 +
233 + .value {
234 + font-weight: bold;
235 + margin-bottom: 2px;
236 + white-space: nowrap;
237 + }
238 + .label {
239 + white-space: nowrap;
240 + font-size: var(--font-size-0);
241 + font-family: var(--font-mono);
242 + opacity: 0.8;
243 + }
244 + }
245 + &.actions {
246 + flex-grow: 0;
247 + .box {
248 + padding: var(--size-2) var(--size-2);
249 + background-color: rgba(0, 0, 0, 0.07);
250 + display: flex;
251 + align-items: center;
252 + border-radius: var(--radius-6);
253 + }
254 + }
255 + }
256 +
257 + &.health-green {
258 + border-color: $text-color-success;
259 + }
260 +
261 + &.health-yellow {
262 + border-color: $text-color-warning;
263 + }
264 +
265 + &.health-red {
266 + border-color: $text-color-danger;
267 + }
268 +}
269 +</style>
src/components/inputs/StreamDetails.vue new
+126
@@ -0,0 +1,126 @@
1 +<template>
2 + <div class="stream-details-box" v-loading="loading" :class="{ active: currentStream }">
3 + <div class="box-header">
4 + <div class="title">
5 + <span v-if="currentStream"> Below the details for stream </span>
6 + <span v-else> Select a stream to see the details </span>
7 + </div>
8 + <div class="select-box" v-if="streams && streams.length">
9 + <el-select v-model="currentStream" placeholder="Streams list" clearable value-key="stream" filterable>
10 + <el-option v-for="stream in streams" :key="stream.id" :label="stream.title" :value="stream"></el-option>
11 + </el-select>
12 + </div>
13 + </div>
14 + <div class="details-box" v-if="currentStream">
15 + <div class="info">
16 + <StreamCard :stream="currentStream" showActions @delete="clearcurrentStream()" />
17 + </div>
18 + </div>
19 + </div>
20 +</template>
21 +
22 +<script setup lang="ts">
23 +import { computed, onBeforeMount, ref, toRefs } from "vue"
24 +import { Streams } from "@/types/graylog.d"
25 +import { ElMessage } from "element-plus"
26 +import StreamCard from "@/components/inputs/StreamCard.vue"
27 +import Api from "@/api"
28 +import { nanoid } from "nanoid"
29 +
30 +type StreamModel = Streams | null | ""
31 +
32 +const emit = defineEmits<{
33 + (e: "update:modelValue", value: StreamModel): void
34 +}>()
35 +
36 +const props = defineProps<{
37 + streams: Streams[] | null
38 + modelValue: StreamModel
39 +}>()
40 +const { streams, modelValue } = toRefs(props)
41 +
42 +const loading = computed(() => !streams?.value || streams.value === null)
43 +
44 +const currentStream = computed<StreamModel>({
45 + get() {
46 + return modelValue.value
47 + },
48 + set(value) {
49 + console.log("Setting currentStream:", value) // Debug log
50 + emit("update:modelValue", value)
51 + }
52 +})
53 +
54 +function clearcurrentStream() {
55 + currentStream.value = null
56 +}
57 +
58 +onBeforeMount(() => {
59 + // getShards()
60 +})
61 +</script>
62 +
63 +<style lang="scss" scoped>
64 +@import "@/assets/scss/_variables";
65 +@import "@/assets/scss/card-shadow";
66 +
67 +.stream-details-box {
68 + padding: var(--size-5) var(--size-6);
69 + border: 2px solid transparent;
70 + @extend .card-base;
71 + &.active {
72 + border-color: $text-color-accent;
73 + @extend .card-shadow--small;
74 + }
75 +
76 + .box-header {
77 + display: flex;
78 + align-items: center;
79 +
80 + .title {
81 + margin-right: var(--size-4);
82 + }
83 +
84 + .select-box {
85 + .el-select {
86 + min-width: var(--size-fluid-9);
87 + max-width: 100%;
88 + }
89 + }
90 + }
91 +
92 + .details-box {
93 + margin-top: var(--size-6);
94 +
95 + .shards {
96 + margin-top: var(--size-4);
97 + @extend .card-base;
98 + @extend .card-shadow--small;
99 +
100 + .shard-state {
101 + font-weight: bold;
102 + &.STARTED {
103 + color: $text-color-success;
104 + }
105 + &.UNASSIGNED {
106 + color: $text-color-warning;
107 + }
108 + }
109 + }
110 + }
111 +
112 + @media (max-width: 1000px) {
113 + .box-header {
114 + flex-direction: column;
115 + align-items: flex-start;
116 + gap: var(--size-2);
117 + .select-box {
118 + width: 100%;
119 + .el-select {
120 + min-width: 100%;
121 + }
122 + }
123 + }
124 + }
125 +}
126 +</style>
src/components/inputs/StreamIcon.vue new
+29
@@ -0,0 +1,29 @@
1 +<template>
2 + <span class="input-icon" :class="[`state-${disabled}`, { color }]">
3 + <i v-if="disabled === Streams.disabled" class="mdi mdi-shield-check"></i>
4 + </span>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import { toRefs } from "vue"
9 +import { Streams } from "@/types/graylog.d"
10 +
11 +const props = defineProps<{
12 + disabled: Streams["disabled"]
13 + color?: boolean
14 +}>()
15 +const { disabled, color } = toRefs(props)
16 +</script>
17 +
18 +<style lang="scss" scoped>
19 +@import "@/assets/scss/_variables";
20 +@import "@/assets/scss/card-shadow";
21 +
22 +.input-icon {
23 + &.color {
24 + &.state-true {
25 + color: $text-color-success;
26 + }
27 + }
28 +}
29 +</style>
src/core/nav.vue
+5 -2
@@ -18,8 +18,11 @@
18 <el-menu-item index="/agents">
19 <span slot="title">Agents</span>
20 </el-menu-item>
21 - <el-menu-item index="/indices-bkp">
22 - <span slot="title">Indicies-bkp</span>
21 + <el-menu-item index="/inputs">
22 + <span slot="title">Inputs</span>
23 + </el-menu-item>
24 + <el-menu-item index="/agents-bkp">
25 + <span slot="title">agents-bkp</span>
26 </el-menu-item>
27 <el-menu-item index="/ecommerce-dashboard">
28 <span slot="title">eCommerce</span>
src/router/index.ts
+27 -4
@@ -12,8 +12,9 @@ import Mail from "../views/apps/Mail.vue"
12 import Ecommerce from "./ecommerce"
13 import Connectors from "../views/apps/Connectors.vue"
14 import Indices from "../views/apps/Dashboards/Indices.vue"
15 -import IndicesBKP from "../views/apps/Dashboards/_bkp_Indices.vue"
15 +import Agents_bkp from "../views/apps/Dashboards/Agents_bkp.vue"
16 import Agents from "../views/apps/Dashboards/Agents.vue"
17 +import Inputs from "../views/apps/Dashboards/Inputs.vue"
18 /*
19
20 //pages
@@ -118,9 +119,31 @@ const router = createRouter({
119 }
120 },
121 {
121 - path: "/indices-bkp",
122 - name: "indices-bkp",
123 - component: IndicesBKP,
122 + path: "/agent/:id?",
123 + name: "agent",
124 + component: () => import("@/views/AgentOverview.vue"),
125 + meta: {
126 + auth: true,
127 + layout: layouts.navLeft,
128 + searchable: true,
129 + tags: ["app"]
130 + }
131 + },
132 + {
133 + path: "/inputs",
134 + name: "inputs",
135 + component: Inputs,
136 + meta: {
137 + auth: true,
138 + layout: layouts.navLeft,
139 + searchable: true,
140 + tags: ["app"]
141 + }
142 + },
143 + {
144 + path: "/agents-bkp",
145 + name: "agents-bkp",
146 + component: Agents_bkp,
147 meta: {
148 auth: true,
149 layout: layouts.navLeft,
src/types/agents.d.ts
+24 -6
@@ -1,4 +1,4 @@
1 -export interface Agents {
1 +export interface Agent {
2 agent_id: string
3 client_id: string
4 client_last_seen: string
@@ -12,9 +12,11 @@ export interface Agents {
12 velociraptor_client_version: string
13 wazuh_agent_version: string
14 vulnerabilities?: AgentVulnerabilities[]
15 + online?: boolean
16 }
17
18 export interface AgentVulnerabilities {
19 + id?: string
20 architecture: string
21 condition: string
22 cve: string
@@ -24,14 +26,30 @@ export interface AgentVulnerabilities {
26 external_references: string[]
27 name: string
28 published: string
27 - severity: string
28 - status: string
29 + severity: VulnerabilitySeverity
30 + status: VulnerabilityStatus
31 title: string
30 - type: string
32 + type: VulnerabilityType
33 updated: string
34 version: string
35 }
36
35 -export type OutdatedWazuhAgents = Agents[]
37 +export enum VulnerabilitySeverity {
38 + Critical = "Critical",
39 + High = "High",
40 + Low = "Low",
41 + Medium = "Medium",
42 + Untriaged = "Untriaged"
43 +}
44 +
45 +export enum VulnerabilityStatus {
46 + Valid = "VALID"
47 +}
48 +
49 +export enum VulnerabilityType {
50 + Package = "PACKAGE"
51 +}
52 +
53 +export type OutdatedWazuhAgents = Agent[]
54
37 -export type OutdatedVelociraptorAgents = Agents[]
55 +export type OutdatedVelociraptorAgents = Agent[]
src/types/graylog.d.ts new
+131
@@ -0,0 +1,131 @@
1 +export interface Message {
2 + caller: string
3 + content: string
4 + node_id: string
5 + timestamp: string
6 +}
7 +
8 +export interface ThroughputMetric {
9 + metric: string
10 + value: number
11 +}
12 +
13 +export interface Documents {
14 + count: number
15 + deleted: number
16 +}
17 +
18 +export interface OperationDetails {
19 + time_seconds: number
20 + total: number
21 +}
22 +
23 +// ShardDetails encapsulates all the details for both all_shards and primary_shards
24 +export interface ShardDetails {
25 + documents: Documents
26 + flush: OperationDetails
27 + get: OperationDetails
28 + index: OperationDetails
29 + merge: OperationDetails
30 + open_search_contexts: number
31 + refresh: OperationDetails
32 + search_fetch: OperationDetails
33 + search_query: OperationDetails
34 + segments: number
35 + store_size_bytes: number
36 +}
37 +
38 +// Routing information for each index
39 +export interface Routing {
40 + active: boolean
41 + id: number
42 + node_hostname: string
43 + node_id: string
44 + node_name: string
45 + primary: boolean
46 + relocating_to: null | string
47 + state: string
48 +}
49 +
50 +// Interface for each individual index like 'wazuh_00001'
51 +export interface IndexDetails {
52 + all_shards: ShardDetails
53 + primary_shards: ShardDetails
54 + reopened: boolean
55 + routing: Routing[]
56 +}
57 +
58 +// Main interface encapsulating the entire JSON structure of IndexData
59 +export interface IndexData {
60 + index_names: string[]
61 + indices: { [key: string]: IndexDetails }
62 + message: string
63 + success: boolean
64 +}
65 +
66 +// Graylog Inputs
67 +
68 +export enum InputState {
69 + RUNNING = "RUNNING",
70 + STOPPED = "STOPPED"
71 +}
72 +
73 +export interface ConfiguredInput {
74 + port: number
75 + title: string
76 +}
77 +
78 +export interface RunningInput {
79 + port: number
80 + state: string
81 + title: string
82 +}
83 +
84 +export interface ConfiguredInputsData {
85 + configured_inputs: ConfiguredInput[]
86 + message: string
87 + success: boolean
88 +}
89 +
90 +export interface RunningInputsData {
91 + inputs: RunningInput[]
92 + message: string
93 + success: boolean
94 +}
95 +
96 +export interface Inputs {
97 + configured_inputs: ConfiguredInputsData
98 + running_inputs: RunningInputsData
99 +}
100 +
101 +// Stream Rule
102 +export interface StreamRule {
103 + description: null | string
104 + field: string
105 + id: string
106 + inverted: boolean
107 + stream_id: string
108 + type: number
109 + value: string
110 +}
111 +
112 +// Stream
113 +export interface Stream {
114 + content_pack: null | string
115 + created_at: string
116 + creator_user_id: string
117 + description: string
118 + disabled: boolean
119 + id: string
120 + index_set_id: string
121 + is_default: boolean
122 + is_editable: boolean
123 + matching_type: string
124 + outputs: any[] // Replace with the appropriate type if known
125 + remove_matches_from_default_stream: boolean
126 + rules: StreamRule[]
127 + title: string
128 +}
129 +
130 +// Streams Array
131 +export interface Streams extends Array<Stream> {}
src/types/indices.d.ts
-1
@@ -8,7 +8,6 @@ export interface Index {
8 store_size_value?: number
9 }
10
11 -// TODO: Better to use a status instead of a color
11 export enum IndexHealth {
12 GREEN = "green",
13 YELLOW = "yellow",
src/views/AgentOverview.vue new
+399
@@ -0,0 +1,399 @@
1 +<template>
2 + <div class="page-agent">
3 + <div class="agent-toolbar">
4 + <div class="back-btn" @click="gotoAgents()">
5 + <i class="mdi mdi-arrow-left"></i>
6 + <span> Agents list </span>
7 + </div>
8 + <div class="delete-btn" @click.stop="handleDelete" v-if="agent">Delete Agent</div>
9 + </div>
10 + <div
11 + class="page-header card-base card-shadow--small flex"
12 + :class="{ critical: agent?.critical_asset, online: isOnline }"
13 + v-loading="loadingAgent"
14 + >
15 + <div class="box grow">
16 + <div class="title">
17 + <div class="critical" :class="{ active: agent?.critical_asset }">
18 + <el-tooltip content="Toggle Critical Assets" placement="top" :show-arrow="false">
19 + <el-button
20 + text
21 + :icon="StarIcon"
22 + :type="agent?.critical_asset ? 'warning' : ''"
23 + circle
24 + @click.stop="toggleCritical(agent?.agent_id, agent?.critical_asset)"
25 + />
26 + </el-tooltip>
27 + </div>
28 + <h1 v-if="agent?.hostname">
29 + {{ agent?.hostname }}
30 + </h1>
31 + <span class="online-badge" v-if="isOnline"> ONLINE </span>
32 + </div>
33 + <el-breadcrumb separator="/">
34 + <el-breadcrumb-item :to="{ path: '/' }"><i class="mdi mdi-home-outline"></i></el-breadcrumb-item>
35 + <el-breadcrumb-item>Agent</el-breadcrumb-item>
36 + <el-breadcrumb-item v-if="agent?.agent_id">#{{ agent?.agent_id }}</el-breadcrumb-item>
37 + </el-breadcrumb>
38 + </div>
39 + <div class="menu-btn align-vertical" @click="sidebarOpen = !sidebarOpen">
40 + <i class="mdi mdi-menu align-vertical-middle"></i>
41 + </div>
42 + </div>
43 + <div class="wrapper">
44 + <div class="sidebar scrollable" :class="{ open: sidebarOpen }">
45 + <el-button size="small" class="close-btn" @click="sidebarOpen = false">close</el-button>
46 + <ul>
47 + <li :class="{ active: activePage === 'overview' }" @click="activePage = 'overview'">Overview</li>
48 + <li :class="{ active: activePage === 'vulnerabilities' }" @click="activePage = 'vulnerabilities'">Vulnerabilities</li>
49 + <li :class="{ active: activePage === 'alerts' }" @click="activePage = 'alerts'">Alerts</li>
50 + </ul>
51 + </div>
52 + <div class="main-content box grow card-base card-shadow--small scrollable only-y">
53 + <div v-if="agent" v-loading="loadingAgent">
54 + <OverviewSection :agent="agent" v-if="activePage === 'overview'" />
55 +
56 + <VulnerabilitiesSection :agent="agent" v-show="activePage === 'vulnerabilities'" />
57 +
58 + <template v-if="activePage === 'alerts'">...yet to be implemented...</template>
59 + </div>
60 + </div>
61 + </div>
62 + </div>
63 +</template>
64 +
65 +<script setup lang="ts">
66 +import { ref, onBeforeMount, computed } from "vue"
67 +import { useRoute } from "vue-router"
68 +import { ElMessage } from "element-plus"
69 +import Api from "@/api"
70 +import { Agent } from "@/types/agents"
71 +import { handleDeleteAgent, isAgentOnline, toggleAgentCritical } from "@/components/agents/utils"
72 +import { Star as StarIcon } from "@element-plus/icons-vue"
73 +import { useRouter } from "vue-router"
74 +import VulnerabilitiesSection from "@/components/agents/VulnerabilitiesSection.vue"
75 +import OverviewSection from "@/components/agents/OverviewSection.vue"
76 +
77 +type AgentPages = "overview" | "vulnerabilities" | "alerts"
78 +
79 +const router = useRouter()
80 +const sidebarOpen = ref(false)
81 +const route = useRoute()
82 +const loadingAgent = ref(false)
83 +const activePage = ref<AgentPages>("overview")
84 +const agent = ref<Agent | null>(null)
85 +
86 +const isOnline = computed(() => {
87 + return isAgentOnline(agent.value?.last_seen)
88 +})
89 +
90 +function getAgent(id: string) {
91 + loadingAgent.value = true
92 +
93 + Api.agents
94 + .getAgents(id)
95 + .then(res => {
96 + if (res.data.success) {
97 + agent.value = res.data.agent || null
98 + } else {
99 + ElMessage({
100 + message: res.data?.message || "An error occurred. Please try again later.",
101 + type: "error"
102 + })
103 + router.push(`/agents`).catch(err => {})
104 + }
105 + })
106 + .catch(err => {
107 + ElMessage({
108 + message: err.response?.data?.message || "An error occurred. Please try again later.",
109 + type: "error"
110 + })
111 + router.push(`/agents`).catch(err => {})
112 + })
113 + .finally(() => {
114 + loadingAgent.value = false
115 + })
116 +}
117 +
118 +function toggleCritical(agentId: string, criticalStatus: boolean) {
119 + toggleAgentCritical({
120 + agentId,
121 + criticalStatus,
122 + cbBefore: () => {
123 + loadingAgent.value = true
124 + },
125 + cbSuccess: () => {
126 + if (agent.value?.critical_asset !== undefined) {
127 + agent.value.critical_asset = !criticalStatus
128 + }
129 + },
130 + cbAfter: () => {
131 + loadingAgent.value = false
132 + }
133 + })
134 +}
135 +
136 +function handleDelete() {
137 + handleDeleteAgent({
138 + agent: agent.value,
139 + cbBefore: () => {
140 + loadingAgent.value = true
141 + },
142 + cbSuccess: () => {
143 + gotoAgents()
144 + },
145 + cbAfter: () => {
146 + loadingAgent.value = false
147 + }
148 + })
149 +}
150 +
151 +function gotoAgents() {
152 + router.push(`/agents`).catch(() => {})
153 +}
154 +
155 +onBeforeMount(() => {
156 + if (route.params.id) {
157 + getAgent(route.params.id.toString())
158 + } else {
159 + router.replace(`/agents`).catch(() => {})
160 + }
161 +})
162 +</script>
163 +
164 +<style lang="scss" scoped>
165 +@import "@/assets/scss/_variables";
166 +
167 +.page-agent {
168 + height: 100%;
169 + margin: 0 !important;
170 + padding: 20px;
171 + padding-bottom: 10px;
172 + box-sizing: border-box;
173 + overflow: hidden;
174 + display: flex;
175 + flex-direction: column;
176 +
177 + .agent-toolbar {
178 + margin-top: 16px;
179 + display: flex;
180 + justify-content: space-between;
181 + align-items: center;
182 +
183 + .back-btn {
184 + cursor: pointer;
185 + opacity: 0.8;
186 + font-size: 14px;
187 +
188 + i {
189 + font-size: 20px;
190 + }
191 +
192 + span {
193 + position: relative;
194 + top: -3px;
195 + margin-left: 4px;
196 + }
197 + }
198 +
199 + .delete-btn {
200 + opacity: 0.8;
201 + cursor: pointer;
202 + font-size: 14px;
203 + }
204 + }
205 +
206 + .page-header {
207 + margin-top: 12px;
208 + margin-bottom: 20px;
209 + min-height: 120px;
210 + border: 2px solid transparent;
211 + box-sizing: border-box;
212 +
213 + .title {
214 + display: flex;
215 + align-items: center;
216 + line-height: 1;
217 +
218 + h1 {
219 + margin: 0;
220 + font-size: var(--font-size-4);
221 + }
222 +
223 + .critical {
224 + margin-right: 6px;
225 + margin-left: -8px;
226 +
227 + &:deep() {
228 + .el-button {
229 + width: 36px;
230 + height: 36px;
231 + }
232 + .el-icon {
233 + width: var(--font-size-3);
234 + height: var(--font-size-3);
235 +
236 + svg {
237 + height: var(--font-size-3);
238 + width: var(--font-size-3);
239 + }
240 + }
241 + }
242 + }
243 +
244 + .online-badge {
245 + border: 2px solid $text-color-success;
246 + color: $text-color-success;
247 + font-weight: bold;
248 + margin-left: 10px;
249 + border-radius: 6px;
250 + font-size: var(--font-size-0);
251 + padding: var(--size-1) var(--size-2);
252 + }
253 + }
254 +
255 + .menu-btn {
256 + color: $text-color-primary;
257 + font-size: 20px;
258 + display: none;
259 + cursor: pointer;
260 + }
261 +
262 + &.critical {
263 + border-color: $text-color-warning;
264 + }
265 + }
266 +
267 + .wrapper {
268 + display: flex;
269 + flex-grow: 1;
270 + overflow: hidden;
271 + padding: 5px;
272 + margin-left: -5px;
273 + margin-right: -5px;
274 +
275 + .sidebar {
276 + box-sizing: border-box;
277 + padding-right: var(--size-3);
278 + min-width: 250px;
279 + max-width: 250px;
280 + max-height: 100vh;
281 +
282 + .close-btn {
283 + display: none;
284 + width: 100%;
285 + margin-bottom: 10px;
286 + }
287 +
288 + ul {
289 + width: 100%;
290 + list-style: none;
291 + padding: 0;
292 + margin: 0;
293 + }
294 + li {
295 + box-sizing: border-box;
296 + width: 100%;
297 + list-style: none;
298 + padding: 15px 20px;
299 + border-bottom: 1px solid transparentize($text-color-primary, 0.9);
300 + cursor: pointer;
301 + position: relative;
302 +
303 + &::after {
304 + content: "";
305 + display: block;
306 + width: 0%;
307 + height: 100%;
308 + background: $text-color-primary;
309 + position: absolute;
310 + top: 0;
311 + left: 0;
312 + opacity: 0;
313 + transition: all 0.5s;
314 + }
315 +
316 + &::before {
317 + content: "";
318 + display: block;
319 + width: 6px;
320 + height: 60%;
321 + background: #6996e0;
322 + position: absolute;
323 + top: 20%;
324 + left: 0;
325 + opacity: 0;
326 + transform: translateX(-100%);
327 + transition: all 0.5s;
328 + }
329 +
330 + &:hover {
331 + &::after {
332 + width: 100%;
333 + opacity: 0.3;
334 + }
335 + }
336 +
337 + &.active {
338 + &::before {
339 + opacity: 1;
340 + transform: translateX(0);
341 + }
342 + }
343 + }
344 + }
345 +
346 + .main-content {
347 + padding: var(--size-5);
348 + flex-grow: 1;
349 + overflow: hidden;
350 + }
351 + }
352 +}
353 +
354 +@media (max-width: 768px) {
355 + .page-agent {
356 + padding-left: 5px;
357 + padding-right: 5px;
358 +
359 + .page-header {
360 + .menu-btn {
361 + display: block;
362 + }
363 + }
364 +
365 + .wrapper {
366 + .sidebar {
367 + padding: var(--size-3);
368 +
369 + .close-btn {
370 + display: block;
371 + }
372 +
373 + margin: 0;
374 + position: absolute;
375 + background: white;
376 + color: #000;
377 + top: 5px;
378 + left: -100%;
379 + opacity: 0;
380 + bottom: 5px;
381 + box-shadow: 40px 0px 160px 80px rgba(0, 0, 0, 0.3);
382 + border-top-right-radius: 4px;
383 + border-bottom-right-radius: 4px;
384 + transition: all 0.5s;
385 +
386 + li {
387 + border-bottom: 1px solid #eee;
388 + }
389 +
390 + &.open {
391 + opacity: 1;
392 + left: 0;
393 + z-index: 999;
394 + }
395 + }
396 + }
397 + }
398 +}
399 +</style>
src/views/apps/Dashboards/Agents.vue
+157 -500
@@ -1,548 +1,205 @@
1 <template>
2 - <div class="page-contacts flex column" id="page-contacts">
3 - <resize-observer @notify="__resizeHanlder" />
4 -
5 - <div class="contacts-root box grow flex gaps justify-center" :class="contactsClass">
6 - <div class="card-base card-shadow--small search-card scrollable only-y">
7 - <h1 class="mt-0">Agents</h1>
8 -
9 - <el-input prefix-icon="el-icon-search" placeholder="Search a contact" clearable v-model="search"> </el-input>
10 -
11 - <div class="o-050 text-right mt-10 mb-30">
12 - <strong>{{ agentsFiltered.length }}</strong> Agents
13 - </div>
14 -
15 - <el-button @click="sync_agents({})">
16 - <i class="mdi mdi-account-plus mr-10"></i>
17 - Sync Agents</el-button
18 - >
19 -
20 - <div class="p-20">
21 - <p>Critical Assets</p>
22 - <ul class="contacts-favourites">
23 - <li v-for="agent in agentsFavorite" :key="agent.agent_id" @click="openDialog(c)">
24 - <img :src="'/static/images/gallery/computer.png'" alt="user favourite avatar" />
25 - <span>{{ agent.hostname }}</span>
26 - </li>
27 - </ul>
28 - </div>
29 - </div>
30 - <div class="contacts-list box grow scrollable only-y">
31 - <div v-for="agent in agentsFiltered" :key="agent.agent_id" class="flex contact" @click="openEditAgentsModal(agent)">
32 - <div class="star align-vertical p-10 fs-22">
33 - <i class="mdi mdi-star align-vertical-middle" v-if="agent.critical_asset"></i>
34 - <i class="mdi mdi-star-outline align-vertical-middle" v-if="!agent.critical_asset"></i>
35 - </div>
36 - <div class="avatar align-vertical">
37 - <img :src="'/static/images/gallery/computer.png'" class="align-vertical-middle" alt="user avatar" />
38 - </div>
39 - <div class="info box grow flex">
40 - <div class="name box grow flex column justify-center p-10">
41 - <div class="fullname fs-18">
42 - <strong>{{ agent.hostname }}</strong>
43 - </div>
44 - <div class="ip fs-14 secondary-text">{{ agent.ip_address }}</div>
45 - <div class="os fs-14 secondary-text">{{ agent.os }}</div>
46 - <div class="os fs-14 secondary-text">{{ agent.label }}</div>
47 - </div>
48 - <div class="phone align-vertical p-10">
49 - <span class="align-vertical-middle">{{ agent.last_seen }}</span>
50 - </div>
51 - <!--Add a el-button to make the agent critical-->
52 - <div class="phone align-vertical p-10">
53 - <el-button type="primary" @click="makeAgentCritical(agent.agent_id)" v-if="!agent.critical_asset">
54 - <i class="mdi mdi-star-outline align-vertical-middle"></i>
55 - </el-button>
56 - <el-button type="primary" @click="makeAgentUncritical(agent.agent_id)" v-if="agent.critical_asset">
57 - <i class="mdi mdi-star align-vertical-middle"></i>
58 - </el-button>
59 - </div>
60 - </div>
61 - </div>
2 + <div class="page-agents flex column">
3 + <div class="wrapper box grow flex justify-center">
4 + <AgentToolbar
5 + v-model="textFilter"
6 + :syncing="loadingSync"
7 + :agents-length="agents.length"
8 + :agents-filtered-length="agentsFiltered.length"
9 + :agents-critical="agentsCritical"
10 + :agents-online="agentsOnline"
11 + @sync="syncAgents()"
12 + @click="gotoAgentPage"
13 + />
14 + <div class="agents-list box grow scrollable only-y" v-loading="loadingAgents">
15 + <transition-group class="animated-list" tag="div" name="list">
16 + <AgentCard
17 + v-for="agent in agentsFiltered"
18 + :key="agent.agent_id"
19 + :agent="agent"
20 + show-actions
21 + @delete="syncAgents()"
22 + @click="gotoAgentPage(agent)"
23 + />
24 + </transition-group>
25 </div>
26 </div>
64 -
65 - <user-dialog v-model="dialogvisible" :userdata="userdata"></user-dialog>
27 </div>
28 </template>
29
69 -<script>
70 -import UserDialog from "@/components/UserDialog.vue"
71 -import Contacts from "@/assets/data/CONTACTS_MOCK_DATA.json"
72 -import { defineComponent } from "@vue/runtime-core"
73 -import _ from "lodash"
74 -import ResizeObserver from "@/components/vue-resize/ResizeObserver.vue"
75 -import axios from "axios"
30 +<script setup lang="ts">
31 +import { computed, onBeforeMount, ref } from "vue"
32 +import { Agent } from "@/types/agents.d"
33 +import { ElMessage } from "element-plus"
34 +import AgentCard from "@/components/agents/AgentCard.vue"
35 +import AgentToolbar from "@/components/agents/AgentToolbar.vue"
36 +import { isAgentOnline } from "@/components/agents/utils"
37 +import Api from "@/api"
38 +import { useRouter } from "vue-router"
39 +
40 +const router = useRouter()
41 +const loadingAgents = ref(false)
42 +const loadingSync = ref(false)
43 +const agents = ref<Agent[]>([])
44 +const textFilter = ref("")
45 +
46 +const agentsFiltered = computed(() => {
47 + return agents.value.filter(
48 + ({ hostname, ip_address, agent_id, label }) =>
49 + (hostname + ip_address + agent_id + label).toString().toLowerCase().indexOf(textFilter.value.toString().toLowerCase()) !== -1
50 + )
51 +})
52 +
53 +const agentsCritical = computed(() => {
54 + return agents.value.filter(({ critical_asset }) => critical_asset)
55 +})
56
77 -export default defineComponent({
78 - name: "Contacts",
79 - data() {
80 - return {
81 - loading: false,
82 - agents: [],
83 - currentAgent: null,
57 +const agentsOnline = computed(() => {
58 + return agents.value.filter(({ online }) => online)
59 +})
60
85 - // Configure Modal
86 - isEditAgentsModalActive: false,
61 +function gotoAgentPage(agent: Agent) {
62 + router.push(`/agent/${agent.agent_id}`).catch(err => {})
63 +}
64
88 - search: "",
89 - dialogvisible: false,
90 - pageWidth: 0,
91 - userdata: {},
92 - contacts: Contacts.slice(0, 30)
93 - }
94 - },
95 - computed: {
96 - contactsFiltered() {
97 - return this.contacts.filter(
98 - ({ full_name, email, phone }) =>
99 - (full_name + email + phone).toString().toLowerCase().indexOf(this.search.toString().toLowerCase()) !== -1
100 - )
101 - },
102 - contactsClass() {
103 - return this.pageWidth >= 870 ? "large" : this.pageWidth >= 760 ? "medium" : "small"
104 - },
105 - contactsFavourite() {
106 - return this.contacts.filter(({ starred }) => starred)
107 - },
108 - agentsFiltered() {
109 - return this.agents.filter(
110 - ({ agent_name, agent_ip, agent_id }) =>
111 - (agent_name + agent_ip + agent_id).toString().toLowerCase().indexOf(this.search.toString().toLowerCase()) !== -1
112 - )
113 - },
114 - agentsFavorite() {
115 - return this.agents.filter(({ critical_asset }) => critical_asset)
116 - }
117 - },
118 - methods: {
119 - openEditAgentsModal(agent) {
120 - this.currentAgent = agent
121 - this.isEditAgentsModalActive = true
122 - },
123 - closeEditAgentsModal() {
124 - this.isEditAgentsModalActive = false
125 - },
65 +function getAgents() {
66 + loadingAgents.value = true
67
127 - openDialog(data) {
128 - this.userdata = data
129 - this.dialogvisible = true
130 - },
131 - setPageWidth() {
132 - this.pageWidth = document.getElementById("page-contacts").offsetWidth
133 - },
134 - __resizeHanlder: _.throttle(function (e) {
135 - this.setPageWidth()
136 - }, 700),
137 - get_agents() {
138 - const path = "http://localhost:5000/agents"
139 - this.loading = true
140 - resizing: true,
141 - axios
142 - .get(path)
143 - .then(response => {
144 - if (response.data.success && Array.isArray(response.data.agents)) {
145 - this.agents = response.data.agents
146 - } else {
147 - console.error("Received non-array agents: ", response.data)
148 - this.agents = [] // Reset to empty array
149 - }
150 - })
151 - .catch(error => {
152 - console.log(error)
153 - this.loading = false
154 - })
155 - },
156 - sync_agents() {
157 - const path = "http://localhost:5000/sync"
158 - this.loading = true
159 - resizing: true,
160 - axios
161 - .get(path)
162 - .then(response => {
163 - this.sync = response.data
164 - this.loading = false
165 - this.succssMesaage = "Agents Synced Successfully"
166 - this.$message({
167 - message: this.succssMesaage,
168 - type: "success"
169 - })
170 - this.get_agents()
171 - })
172 - .catch(error => {
173 - if (error.response.status === 401) {
174 - this.errorMessage = "Unauthorized"
175 - this.$message({
176 - message: this.errorMessage,
177 - type: "error"
178 - })
179 - } else {
180 - this.$message({
181 - message: "Failed to Sync Agents",
182 - type: "error"
183 - })
184 - }
185 - })
186 - },
187 - makeAgentCritical(agent_id) {
188 - const path = "http://localhost:5000/agents/" + agent_id + "/critical"
189 - axios
190 - .put(path)
191 - .then(response => {
192 - this.loading = false
193 - this.succssMesaage = "Agent Criticality Updated Successfully"
194 - this.$message({
195 - message: this.succssMesaage,
196 - type: "success"
197 - })
198 - this.get_agents()
68 + Api.agents
69 + .getAgents()
70 + .then(res => {
71 + if (res.data.success) {
72 + agents.value = (res.data.agents || []).map(o => {
73 + o.online = isAgentOnline(o.last_seen)
74 + return o
75 })
200 - .catch(error => {
201 - if (error.response.status === 401) {
202 - this.errorMessage = "Unauthorized"
203 - this.$message({
204 - message: this.errorMessage,
205 - type: "error"
206 - })
207 - } else {
208 - this.$message({
209 - message: "Failed to Update Agent Criticality",
210 - type: "error"
211 - })
212 - }
76 + } else {
77 + ElMessage({
78 + message: res.data?.message || "An error occurred. Please try again later.",
79 + type: "error"
80 })
214 - },
215 - makeAgentUncritical(agent_id) {
216 - const path = "http://localhost:5000/agents/" + agent_id + "/uncritical"
217 - axios
218 - .put(path)
219 - .then(response => {
220 - this.loading = false
221 - this.succssMesaage = "Agent Criticality Updated Successfully"
222 - this.$message({
223 - message: this.succssMesaage,
224 - type: "success"
225 - })
226 - this.get_agents()
81 + }
82 + })
83 + .catch(err => {
84 + ElMessage({
85 + message: err.response?.data?.message || "An error occurred. Please try again later.",
86 + type: "error"
87 + })
88 + })
89 + .finally(() => {
90 + loadingAgents.value = false
91 + })
92 +}
93 +
94 +function syncAgents() {
95 + loadingSync.value = true
96 +
97 + Api.agents
98 + .syncAgents()
99 + .then(res => {
100 + if (res.data.success) {
101 + ElMessage({
102 + message: "Agents Synced Successfully",
103 + type: "success"
104 })
228 - .catch(error => {
229 - if (error.response.status === 401) {
230 - this.errorMessage = "Unauthorized"
231 - this.$message({
232 - message: this.errorMessage,
233 - type: "error"
234 - })
235 - } else {
236 - this.$message({
237 - message: "Failed to Update Agent Criticality",
238 - type: "error"
239 - })
240 - }
105 + getAgents()
106 + } else {
107 + ElMessage({
108 + message: res.data?.message || "An error occurred. Please try again later.",
109 + type: "error"
110 })
242 - }
243 - },
244 - mounted() {
245 - this.setPageWidth()
246 - this.get_agents()
247 - },
248 - watch: {
249 - agentsFiltered(newValue) {
250 - console.log("agentsFiltered:", newValue)
251 - }
252 - },
253 - components: {
254 - ResizeObserver,
255 - UserDialog
256 - }
111 + }
112 + })
113 + .catch(err => {
114 + if (err.response.status === 401) {
115 + ElMessage({
116 + message: err.response?.data?.message || "Sync returned Unauthorized.",
117 + type: "error"
118 + })
119 + } else {
120 + ElMessage({
121 + message: err.response?.data?.message || "Failed to Sync Agents",
122 + type: "error"
123 + })
124 + }
125 + })
126 + .finally(() => {
127 + loadingSync.value = false
128 + })
129 +}
130 +
131 +onBeforeMount(() => {
132 + getAgents()
133 + syncAgents()
134 })
135 </script>
136
137 <style lang="scss" scoped>
138 @import "../../../assets/scss/_variables";
139
263 -.page-contacts {
140 +.page-agents {
141 height: 100%;
142 margin: 0 !important;
143 padding: 20px;
144 padding-bottom: 10px;
145 box-sizing: border-box;
146 + container-type: inline-size;
147
270 - .search-card {
271 - padding: 50px;
272 - max-width: 350px;
273 - //max-height: 320px;
274 - box-sizing: border-box;
275 - margin-bottom: 15px;
276 -
277 - .el-input,
278 - .el-button {
279 - width: 100%;
280 - }
281 -
282 - .contacts-favourites {
283 - margin: 0;
284 - padding: 0;
285 - list-style: none;
286 - overflow: auto;
287 -
288 - li {
289 - list-style: none;
290 - padding: 0;
291 - margin: 0;
292 - margin-right: 10px;
293 - margin-bottom: 10px;
294 - float: left;
295 - cursor: pointer;
296 - background: $background-color;
297 - color: $text-color-primary;
298 - border-radius: 4px;
299 - overflow: hidden;
300 -
301 - &:hover {
302 - color: $text-color-accent;
303 - }
304 -
305 - img {
306 - width: 30px;
307 - height: 30px;
308 - float: left;
309 - }
310 -
311 - span {
312 - line-height: 30px;
313 - padding: 0 10px;
314 - }
315 - }
316 - }
317 - }
318 -
319 - .search-wrap {
320 - margin: 0 auto;
321 - margin-bottom: 10px;
322 - padding: 0px 30px;
323 - box-sizing: border-box;
324 - width: 100%;
325 - max-width: 600px;
326 -
327 - i {
328 - display: inline-block;
329 - width: 22px;
330 - }
331 -
332 - input {
333 - outline: none;
334 - background: transparent;
335 - border: none;
336 - font-size: 15px;
337 - position: relative;
338 - top: -2px;
339 - width: 100%;
340 - padding: 0;
341 - color: $text-color-primary;
342 - }
343 -
344 - .contacts-tot {
345 - margin-right: 20px;
346 - margin-left: 10px;
347 - }
348 -
349 - a {
350 - border-bottom: 1px solid;
351 - text-decoration: none;
352 - color: $text-color-primary;
353 -
354 - &:hover {
355 - opacity: 0.6;
356 - }
357 - }
358 - }
359 -
360 - .contacts-root {
148 + .wrapper {
149 max-height: 100%;
150 + gap: var(--size-2);
151 }
152
364 - .contacts-list {
365 - //margin: 0 auto;
366 - width: 100%;
367 - max-width: 965px;
368 - padding: 0px 30px;
369 - box-sizing: border-box;
370 -
371 - .contact {
372 - margin: 10px 0;
373 - padding: 5px;
374 - box-sizing: border-box;
375 - cursor: pointer;
376 - transition: all 0.5s 0.25s;
153 + .agents-list {
154 + padding: 0 5px;
155 + .agent-card {
156 + margin-bottom: var(--size-2);
157 + }
158
378 - .star {
379 - .mdi-star {
380 - color: #ffd730;
381 - }
382 - .mdi-star-outline {
383 - opacity: 0.5;
384 - }
159 + .animated-list {
160 + .list-enter-active,
161 + .list-leave-active,
162 + .list-move {
163 + transition: 500ms cubic-bezier(0.59, 0.12, 0.34, 0.95);
164 + transition-property: opacity, transform;
165 }
166
387 - .avatar {
388 - width: 60px;
389 - transition: all 0.5s 0.25s;
390 -
391 - img {
392 - border: 1px solid transparentize($text-color-primary, 0.9);
393 - box-sizing: border-box;
394 - width: 50px;
395 - height: 50px;
396 - border-radius: 50%;
397 - transition: all 0.5s 0.25s;
398 - }
167 + .list-enter {
168 + opacity: 0;
169 + transform: scaleY(0);
170 }
171
401 - .info {
402 - word-break: break-word;
403 -
404 - .name {
405 - //.fullname {}
406 -
407 - .email {
408 - opacity: 0;
409 - line-height: 0;
410 - transition: all 0.5s 0.25s;
411 - }
412 -
413 - .phone {
414 - display: none;
415 - }
416 - }
417 -
418 - //.phone {}
172 + .list-enter-to {
173 + opacity: 1;
174 + transform: scaleY(1);
175 }
176
421 - &:hover {
422 - margin: 15px -20px;
423 - padding: 10px;
424 - background-color: lighten($background-color, 20%);
425 - border-radius: 5px;
426 - box-shadow:
427 - 0 8px 16px 0 rgba(40, 40, 90, 0.09),
428 - 0 3px 6px 0 rgba(0, 0, 0, 0.065);
429 -
430 - .avatar {
431 - width: 90px;
432 -
433 - img {
434 - width: 90px;
435 - height: 90px;
436 - }
437 - }
438 -
439 - .info {
440 - .name {
441 - .email {
442 - opacity: 1;
443 - line-height: 1.4;
444 - }
445 - }
446 - }
177 + .list-leave-active {
178 + position: absolute;
179 + left: 0;
180 + right: 0;
181 }
448 - }
449 - }
182
451 - .contacts-root {
452 - &.medium {
453 - .search-card {
454 - padding: 20px;
455 - max-width: 260px;
456 - //max-height: 260px;
183 + .list-leave-to {
184 + opacity: 0;
185 + transform: scaleY(0);
186 + transform-origin: center top;
187 }
188 }
459 - &.small {
460 - overflow-y: auto;
461 - display: block;
462 - -webkit-box-orient: vertical;
463 - -webkit-box-direction: normal;
464 - -ms-flex-direction: column;
465 - flex-direction: column;
466 - padding: 5px;
467 -
468 - .search-card {
469 - padding: 20px;
470 - max-width: 100%;
471 - width: 100%;
472 - //max-height: 240px;
473 - flex: none;
474 - -webkit-box-flex: none;
475 - -ms-flex: none;
476 - display: block;
477 - overflow: hidden !important;
478 - }
189
480 - .contacts-list {
481 - flex: none;
482 - -webkit-box-flex: none;
483 - -ms-flex: none;
484 - display: block;
485 - overflow: hidden !important;
486 - }
190 + &.el-loading-parent--relative {
191 + overflow-x: hidden !important;
192 + overflow-y: hidden !important;
193 }
194 }
489 -}
490 -
491 -@media (max-width: 768px) {
492 - .page-contacts {
493 - .search-wrap {
494 - padding: 0;
495 - }
496 - .contacts-list {
497 - padding: 0px;
498 -
499 - .contact {
500 - .avatar {
501 - width: 40px;
195
503 - img {
504 - width: 40px;
505 - height: 40px;
506 - }
507 - }
508 -
509 - .info {
510 - .phone {
511 - display: none;
512 - }
513 -
514 - .name {
515 - .phone {
516 - display: block;
517 - }
518 - }
519 - }
520 -
521 - &:hover {
522 - margin: 15px 0px;
523 -
524 - .avatar {
525 - width: 60px;
526 -
527 - img {
528 - width: 60px;
529 - height: 60px;
530 - }
531 - }
532 - }
533 - }
534 - }
196 + @container (max-width: 770px) {
197 + .wrapper {
198 + flex-direction: column;
199
536 - .contacts-root {
537 - &.medium {
538 - .contacts-list {
539 - padding: 0 30px;
540 - }
541 - }
542 - &.small {
543 - .contacts-list {
544 - padding: 8px;
545 - }
200 + .agents-list {
201 + margin-left: -5px;
202 + margin-right: -10px;
203 }
204 }
205 }
src/views/apps/Dashboards/Agents_bkp.vue new
+550
@@ -0,0 +1,550 @@
1 +<template>
2 + <div class="page-contacts flex column" id="page-contacts">
3 + <resize-observer @notify="__resizeHanlder" />
4 +
5 + <div class="contacts-root box grow flex gaps justify-center" :class="contactsClass">
6 + <div class="card-base card-shadow--small search-card scrollable only-y">
7 + <h1 class="mt-0">Agents</h1>
8 +
9 + <el-input prefix-icon="el-icon-search" placeholder="Search a contact" clearable v-model="search"> </el-input>
10 +
11 + <div class="o-050 text-right mt-10 mb-30">
12 + <strong>{{ agentsFiltered.length }}</strong> Agents
13 + </div>
14 +
15 + <el-button @click="sync_agents({})">
16 + <i class="mdi mdi-account-plus mr-10"></i>
17 + Sync Agents</el-button
18 + >
19 +
20 + <div class="p-20">
21 + <p>Critical Assets</p>
22 + <ul class="contacts-favourites">
23 + <li v-for="agent in agentsFavorite" :key="agent.agent_id" @click="openDialog(c)">
24 + <img :src="'/static/images/gallery/computer.png'" alt="user favourite avatar" />
25 + <span>{{ agent.hostname }}</span>
26 + </li>
27 + </ul>
28 + </div>
29 + </div>
30 + <div class="contacts-list box grow scrollable only-y">
31 + <div v-for="agent in agentsFiltered" :key="agent.agent_id" class="flex contact" @click="openEditAgentsModal(agent)">
32 + <div class="star align-vertical p-10 fs-22">
33 + <i class="mdi mdi-star align-vertical-middle" v-if="agent.critical_asset"></i>
34 + <i class="mdi mdi-star-outline align-vertical-middle" v-if="!agent.critical_asset"></i>
35 + </div>
36 + <div class="avatar align-vertical">
37 + <img :src="'/static/images/gallery/computer.png'" class="align-vertical-middle" alt="user avatar" />
38 + </div>
39 + <div class="info box grow flex">
40 + <div class="name box grow flex column justify-center p-10">
41 + <div class="fullname fs-18">
42 + <strong>{{ agent.hostname }}</strong>
43 + </div>
44 + <div class="ip fs-14 secondary-text">{{ agent.ip_address }}</div>
45 + <div class="os fs-14 secondary-text">{{ agent.os }}</div>
46 + <div class="os fs-14 secondary-text">{{ agent.label }}</div>
47 + </div>
48 + <div class="phone align-vertical p-10">
49 + <span class="align-vertical-middle">{{ agent.last_seen }}</span>
50 + </div>
51 + <!--Add a el-button to make the agent critical-->
52 + <div class="phone align-vertical p-10">
53 + <el-button type="primary" @click="makeAgentCritical(agent.agent_id)" v-if="!agent.critical_asset">
54 + <i class="mdi mdi-star-outline align-vertical-middle"></i>
55 + </el-button>
56 + <el-button type="primary" @click="makeAgentUncritical(agent.agent_id)" v-if="agent.critical_asset">
57 + <i class="mdi mdi-star align-vertical-middle"></i>
58 + </el-button>
59 + </div>
60 + </div>
61 + </div>
62 + </div>
63 + </div>
64 +
65 + <user-dialog v-model="dialogvisible" :userdata="userdata"></user-dialog>
66 + </div>
67 +</template>
68 +
69 +<script>
70 +import UserDialog from "@/components/UserDialog.vue"
71 +import Contacts from "@/assets/data/CONTACTS_MOCK_DATA.json"
72 +import { defineComponent } from "vue"
73 +import _ from "lodash"
74 +import ResizeObserver from "@/components/vue-resize/ResizeObserver.vue"
75 +import axios from "axios"
76 +
77 +export default defineComponent({
78 + name: "Contacts",
79 + data() {
80 + return {
81 + loading: false,
82 + agents: [],
83 + currentAgent: null,
84 +
85 + // Configure Modal
86 + isEditAgentsModalActive: false,
87 +
88 + search: "",
89 + dialogvisible: false,
90 + pageWidth: 0,
91 + userdata: {},
92 + contacts: Contacts.slice(0, 30)
93 + }
94 + },
95 + computed: {
96 + contactsFiltered() {
97 + return this.contacts.filter(
98 + ({ full_name, email, phone }) =>
99 + (full_name + email + phone).toString().toLowerCase().indexOf(this.search.toString().toLowerCase()) !== -1
100 + )
101 + },
102 + contactsClass() {
103 + return this.pageWidth >= 870 ? "large" : this.pageWidth >= 760 ? "medium" : "small"
104 + },
105 + contactsFavourite() {
106 + return this.contacts.filter(({ starred }) => starred)
107 + },
108 + agentsFiltered() {
109 + return this.agents.filter(
110 + ({ agent_name, agent_ip, agent_id }) =>
111 + (agent_name + agent_ip + agent_id).toString().toLowerCase().indexOf(this.search.toString().toLowerCase()) !== -1
112 + )
113 + },
114 + agentsFavorite() {
115 + return this.agents.filter(({ critical_asset }) => critical_asset)
116 + }
117 + },
118 + methods: {
119 + openEditAgentsModal(agent) {
120 + this.currentAgent = agent
121 + this.isEditAgentsModalActive = true
122 + },
123 + closeEditAgentsModal() {
124 + this.isEditAgentsModalActive = false
125 + },
126 +
127 + openDialog(data) {
128 + this.userdata = data
129 + this.dialogvisible = true
130 + },
131 + setPageWidth() {
132 + this.pageWidth = document.getElementById("page-contacts").offsetWidth
133 + },
134 + __resizeHanlder: _.throttle(function (e) {
135 + this.setPageWidth()
136 + }, 700),
137 + get_agents() {
138 + const path = "http://127.0.0.1:5000/agents"
139 + this.loading = true
140 + true,
141 + axios
142 + .get(path)
143 + .then(response => {
144 + if (response.data.success && Array.isArray(response.data.agents)) {
145 + this.agents = response.data.agents
146 + } else {
147 + console.error("Received non-array agents: ", response.data)
148 + this.agents = [] // Reset to empty array
149 + }
150 + })
151 + .catch(error => {
152 + console.log(error)
153 + this.loading = false
154 + })
155 + },
156 + sync_agents() {
157 + const path = "http://127.0.0.1:5000/sync"
158 + this.loading = true
159 + true,
160 + axios
161 + .get(path)
162 + .then(response => {
163 + this.sync = response.data
164 + this.loading = false
165 + this.succssMesaage = "Agents Synced Successfully"
166 + this.$message({
167 + message: this.succssMesaage,
168 + type: "success"
169 + })
170 + this.get_agents()
171 + })
172 + .catch(error => {
173 + if (error.response.status === 401) {
174 + this.errorMessage = "Unauthorized"
175 + this.$message({
176 + message: this.errorMessage,
177 + type: "error"
178 + })
179 + } else {
180 + this.$message({
181 + message: "Failed to Sync Agents",
182 + type: "error"
183 + })
184 + }
185 + })
186 + },
187 + makeAgentCritical(agent_id) {
188 + const path = "http://127.0.0.1:5000/agents/" + agent_id + "/critical"
189 + axios
190 + .put(path)
191 + .then(response => {
192 + this.loading = false
193 + this.succssMesaage = "Agent Criticality Updated Successfully"
194 + this.$message({
195 + message: this.succssMesaage,
196 + type: "success"
197 + })
198 + this.get_agents()
199 + })
200 + .catch(error => {
201 + if (error.response.status === 401) {
202 + this.errorMessage = "Unauthorized"
203 + this.$message({
204 + message: this.errorMessage,
205 + type: "error"
206 + })
207 + } else {
208 + this.$message({
209 + message: "Failed to Update Agent Criticality",
210 + type: "error"
211 + })
212 + }
213 + })
214 + },
215 + makeAgentUncritical(agent_id) {
216 + const path = "http://127.0.0.1:5000/agents/" + agent_id + "/uncritical"
217 + axios
218 + .put(path)
219 + .then(response => {
220 + this.loading = false
221 + this.succssMesaage = "Agent Criticality Updated Successfully"
222 + this.$message({
223 + message: this.succssMesaage,
224 + type: "success"
225 + })
226 + this.get_agents()
227 + })
228 + .catch(error => {
229 + if (error.response.status === 401) {
230 + this.errorMessage = "Unauthorized"
231 + this.$message({
232 + message: this.errorMessage,
233 + type: "error"
234 + })
235 + } else {
236 + this.$message({
237 + message: "Failed to Update Agent Criticality",
238 + type: "error"
239 + })
240 + }
241 + })
242 + }
243 + },
244 + mounted() {
245 + this.setPageWidth()
246 + this.get_agents()
247 + },
248 + watch: {
249 + agentsFiltered(newValue) {
250 + console.log("agentsFiltered:", newValue)
251 + }
252 + },
253 + components: {
254 + ResizeObserver,
255 + UserDialog
256 + }
257 +})
258 +</script>
259 +
260 +<style lang="scss" scoped>
261 +@import "../../../assets/scss/_variables";
262 +
263 +.page-contacts {
264 + height: 100%;
265 + margin: 0 !important;
266 + padding: 20px;
267 + padding-bottom: 10px;
268 + box-sizing: border-box;
269 +
270 + .search-card {
271 + padding: 50px;
272 + max-width: 350px;
273 + //max-height: 320px;
274 + box-sizing: border-box;
275 + margin-bottom: 15px;
276 +
277 + .el-input,
278 + .el-button {
279 + width: 100%;
280 + }
281 +
282 + .contacts-favourites {
283 + margin: 0;
284 + padding: 0;
285 + list-style: none;
286 + overflow: auto;
287 +
288 + li {
289 + list-style: none;
290 + padding: 0;
291 + margin: 0;
292 + margin-right: 10px;
293 + margin-bottom: 10px;
294 + float: left;
295 + cursor: pointer;
296 + background: $background-color;
297 + color: $text-color-primary;
298 + border-radius: 4px;
299 + overflow: hidden;
300 +
301 + &:hover {
302 + color: $text-color-accent;
303 + }
304 +
305 + img {
306 + width: 30px;
307 + height: 30px;
308 + float: left;
309 + }
310 +
311 + span {
312 + line-height: 30px;
313 + padding: 0 10px;
314 + }
315 + }
316 + }
317 + }
318 +
319 + .search-wrap {
320 + margin: 0 auto;
321 + margin-bottom: 10px;
322 + padding: 0px 30px;
323 + box-sizing: border-box;
324 + width: 100%;
325 + max-width: 600px;
326 +
327 + i {
328 + display: inline-block;
329 + width: 22px;
330 + }
331 +
332 + input {
333 + outline: none;
334 + background: transparent;
335 + border: none;
336 + font-size: 15px;
337 + position: relative;
338 + top: -2px;
339 + width: 100%;
340 + padding: 0;
341 + color: $text-color-primary;
342 + }
343 +
344 + .contacts-tot {
345 + margin-right: 20px;
346 + margin-left: 10px;
347 + }
348 +
349 + a {
350 + border-bottom: 1px solid;
351 + text-decoration: none;
352 + color: $text-color-primary;
353 +
354 + &:hover {
355 + opacity: 0.6;
356 + }
357 + }
358 + }
359 +
360 + .contacts-root {
361 + max-height: 100%;
362 + }
363 +
364 + .contacts-list {
365 + //margin: 0 auto;
366 + width: 100%;
367 + max-width: 965px;
368 + padding: 0px 30px;
369 + box-sizing: border-box;
370 +
371 + .contact {
372 + margin: 10px 0;
373 + padding: 5px;
374 + box-sizing: border-box;
375 + cursor: pointer;
376 + transition: all 0.5s 0.25s;
377 +
378 + .star {
379 + .mdi-star {
380 + color: #ffd730;
381 + }
382 + .mdi-star-outline {
383 + opacity: 0.5;
384 + }
385 + }
386 +
387 + .avatar {
388 + width: 60px;
389 + transition: all 0.5s 0.25s;
390 +
391 + img {
392 + border: 1px solid transparentize($text-color-primary, 0.9);
393 + box-sizing: border-box;
394 + width: 50px;
395 + height: 50px;
396 + border-radius: 50%;
397 + transition: all 0.5s 0.25s;
398 + }
399 + }
400 +
401 + .info {
402 + word-break: break-word;
403 +
404 + .name {
405 + //.fullname {}
406 +
407 + .email {
408 + opacity: 0;
409 + line-height: 0;
410 + transition: all 0.5s 0.25s;
411 + }
412 +
413 + .phone {
414 + display: none;
415 + }
416 + }
417 +
418 + //.phone {}
419 + }
420 +
421 + &:hover {
422 + margin: 15px -20px;
423 + padding: 10px;
424 + background-color: lighten($background-color, 20%);
425 + border-radius: 5px;
426 + box-shadow:
427 + 0 8px 16px 0 rgba(40, 40, 90, 0.09),
428 + 0 3px 6px 0 rgba(0, 0, 0, 0.065);
429 +
430 + .avatar {
431 + width: 90px;
432 +
433 + img {
434 + width: 90px;
435 + height: 90px;
436 + }
437 + }
438 +
439 + .info {
440 + .name {
441 + .email {
442 + opacity: 1;
443 + line-height: 1.4;
444 + }
445 + }
446 + }
447 + }
448 + }
449 + }
450 +
451 + .contacts-root {
452 + &.medium {
453 + .search-card {
454 + padding: 20px;
455 + max-width: 260px;
456 + //max-height: 260px;
457 + }
458 + }
459 + &.small {
460 + overflow-y: auto;
461 + display: block;
462 + -webkit-box-orient: vertical;
463 + -webkit-box-direction: normal;
464 + -ms-flex-direction: column;
465 + flex-direction: column;
466 + padding: 5px;
467 +
468 + .search-card {
469 + padding: 20px;
470 + max-width: 100%;
471 + width: 100%;
472 + //max-height: 240px;
473 + flex: none;
474 + -webkit-box-flex: none;
475 + -ms-flex: none;
476 + display: block;
477 + overflow: hidden !important;
478 + }
479 +
480 + .contacts-list {
481 + flex: none;
482 + -webkit-box-flex: none;
483 + -ms-flex: none;
484 + display: block;
485 + overflow: hidden !important;
486 + }
487 + }
488 + }
489 +}
490 +
491 +@media (max-width: 768px) {
492 + .page-contacts {
493 + .search-wrap {
494 + padding: 0;
495 + }
496 + .contacts-list {
497 + padding: 0px;
498 +
499 + .contact {
500 + .avatar {
501 + width: 40px;
502 +
503 + img {
504 + width: 40px;
505 + height: 40px;
506 + }
507 + }
508 +
509 + .info {
510 + .phone {
511 + display: none;
512 + }
513 +
514 + .name {
515 + .phone {
516 + display: block;
517 + }
518 + }
519 + }
520 +
521 + &:hover {
522 + margin: 15px 0px;
523 +
524 + .avatar {
525 + width: 60px;
526 +
527 + img {
528 + width: 60px;
529 + height: 60px;
530 + }
531 + }
532 + }
533 + }
534 + }
535 +
536 + .contacts-root {
537 + &.medium {
538 + .contacts-list {
539 + padding: 0 30px;
540 + }
541 + }
542 + &.small {
543 + .contacts-list {
544 + padding: 8px;
545 + }
546 + }
547 + }
548 + }
549 +}
550 +</style>
src/views/apps/Dashboards/Indices.vue
+7 -2
@@ -24,7 +24,7 @@
24 <div class="col basis-40">
25 <NodeAllocation class="stretchy" />
26 </div>
27 - <div class="col basis-60">
27 + <div class="col basis-60 chart-card">
28 <TopIndices :indices="indices" />
29 </div>
30 </div>
@@ -109,7 +109,7 @@ onBeforeMount(() => {
109
110 .col {
111 flex-grow: 1;
112 - overflow: hidden;
112 + //overflow: hidden;
113 &.basis-20 {
114 flex-basis: 20%;
115 }
@@ -125,6 +125,11 @@ onBeforeMount(() => {
125 &.basis-80 {
126 flex-basis: 80%;
127 }
128 +
129 + &.chart-card {
130 + @extend .card-base;
131 + @extend .card-shadow--small;
132 + }
133 }
134
135 .stretchy {
src/views/apps/Dashboards/Inputs.vue new
+175
@@ -0,0 +1,175 @@
1 +<template>
2 + <el-scrollbar class="page page-inputs">
3 + <div class="section">
4 + <InputsMarquee :inputs="runningInputs" @click="setInput" />
5 + </div>
6 +
7 + <div class="section">
8 + <Details :inputs="configuredInputs" v-model="currentInput" />
9 + </div>
10 +
11 + <div class="section">
12 + <div class="columns">
13 + <div class="col basis-50">
14 + <StreamDetails class="stretchy" :streams="streams" v-model="currentStream" />
15 + </div>
16 + <!-- <div class="col basis-50">
17 + <UnhealthyIndices :indices="indices" @click="setIndex" class="stretchy" />
18 + </div> -->
19 + </div>
20 + </div>
21 + </el-scrollbar>
22 +</template>
23 +
24 +<script lang="ts" setup>
25 +import { RunningInput, ConfiguredInput, Streams } from "@/types/graylog.d"
26 +import Api from "@/api"
27 +import { ElMessage } from "element-plus"
28 +import { onBeforeMount, ref } from "vue"
29 +import InputsMarquee from "@/components/inputs/Marquee.vue"
30 +import StreamDetails from "@/components/inputs/StreamDetails.vue"
31 +import Details from "@/components/inputs/Details.vue"
32 +
33 +const runningInputs = ref<RunningInput[] | null>(null)
34 +const configuredInputs = ref<ConfiguredInput[] | null>(null)
35 +const loadingInput = ref(false)
36 +const currentInput = ref<RunningInput | null>(null)
37 +const streams = ref<Streams | null>(null)
38 +const currentStream = ref<Streams | null>(null)
39 +
40 +function setInput(input: RunningInput) {
41 + currentInput.value = input
42 +}
43 +
44 +function setStream(stream: Streams) {
45 + currentStream.value = stream
46 +}
47 +
48 +function getInputsRunning() {
49 + loadingInput.value = true
50 +
51 + Api.graylog
52 + .getInputsRunning()
53 + .then(res => {
54 + if (res.data.running_inputs.success) {
55 + runningInputs.value = res.data.running_inputs.inputs
56 + } else {
57 + ElMessage({
58 + message: res.data.running_inputs?.message || "An error occurred. Please try again later.",
59 + type: "error"
60 + })
61 + }
62 + })
63 + .catch(err => {
64 + // Handle errors
65 + })
66 + .finally(() => {
67 + loadingInput.value = false
68 + })
69 +}
70 +
71 +function getInputsConfigured() {
72 + loadingInput.value = true
73 +
74 + Api.graylog
75 + .getInputsConfigured()
76 + .then(res => {
77 + if (res.data.configured_inputs.success) {
78 + configuredInputs.value = res.data.configured_inputs.configured_inputs
79 + } else {
80 + ElMessage({
81 + message: res.data.configured_inputs?.message || "An error occurred. Please try again later.",
82 + type: "error"
83 + })
84 + }
85 + })
86 + .catch(err => {
87 + // Handle errors
88 + })
89 + .finally(() => {
90 + loadingInput.value = false
91 + })
92 +}
93 +
94 +function getStreams() {
95 + loadingInput.value = true
96 +
97 + Api.graylog
98 + .getStreams()
99 + .then(res => {
100 + if (res.data.success) {
101 + streams.value = res.data.streams.streams
102 + } else {
103 + ElMessage({
104 + message: res.data?.message || "An error occurred. Please try again later.",
105 + type: "error"
106 + })
107 + }
108 + })
109 + .catch(err => {
110 + // Handle errors
111 + })
112 + .finally(() => {
113 + loadingInput.value = false
114 + })
115 +}
116 +
117 +onBeforeMount(() => {
118 + getInputsRunning(), getInputsConfigured(), getStreams()
119 +})
120 +</script>
121 +
122 +<style lang="scss" scoped>
123 +@import "@/assets/scss/_variables";
124 +@import "@/assets/scss/card-shadow";
125 +
126 +.page-inputs {
127 + .section {
128 + margin-bottom: var(--size-6);
129 + .columns {
130 + display: flex;
131 + gap: var(--size-6);
132 +
133 + .col {
134 + flex-grow: 1;
135 + overflow: hidden;
136 + &.basis-20 {
137 + flex-basis: 20%;
138 + }
139 + &.basis-40 {
140 + flex-basis: 40%;
141 + }
142 + &.basis-50 {
143 + flex-basis: 50%;
144 + }
145 + &.basis-60 {
146 + flex-basis: 60%;
147 + }
148 + &.basis-80 {
149 + flex-basis: 80%;
150 + }
151 + }
152 +
153 + .stretchy {
154 + height: 100%;
155 + box-sizing: border-box;
156 + }
157 + }
158 + }
159 +
160 + @media (max-width: 1000px) {
161 + .section {
162 + .columns {
163 + flex-direction: column;
164 + }
165 + }
166 + }
167 + @media (max-width: 1200px) {
168 + .section {
169 + .columns.column-1200 {
170 + flex-direction: column;
171 + }
172 + }
173 + }
174 +}
175 +</style>
src/views/apps/Dashboards/_bkp_Indices.vue deleted
-694
@@ -1,694 +0,0 @@
1 -<template>
2 - <el-scrollbar class="page page-indices">
3 - bkp
4 - <div class="card-base mb-30">
5 - <IndicesMarquee :indices="indices" @click="setIndex" />
6 - </div>
7 -
8 - <div class="index-details-box">
9 - <div class="flex center demo-box bg-orange">
10 - <el-select v-model="currentIndex" placeholder="Select Your Index" clearable :value-key="'index'">
11 - <el-option v-for="index in indices" :key="index.index" :label="index.index" :value="index"></el-option>
12 - </el-select>
13 - </div>
14 - </div>
15 -
16 - <!--BEGIN TEST-->
17 - <div class="box center left">
18 - <div class="page-header header-primary card-base card-shadow--small">
19 - <h1 class="title">Index Stats</h1>
20 -
21 - <div class="flex justify-center align-center bg-orange" v-if="currentIndex">
22 - <div class="widget-icon-box mr-20 animate__animated animate__fadeInRight">
23 - <span class="badge">
24 - <i v-if="currentIndex.health === 'green'" class="mdi mdi-shield-check"></i>
25 - <i v-else-if="currentIndex.health === 'yellow'" class="mdi mdi-alert"></i>
26 - <i v-else-if="currentIndex.health === 'red'" class="mdi mdi-alert-decagram"></i>
27 - <strong class="accent-text font-size-20">Index:</strong>
28 - </span>
29 - <span class="highlight font-size-20">{{ currentIndex.index }}</span>
30 - </div>
31 - <div class="widget-icon-box mr-20 animate__animated animate__fadeInRight">
32 - <span class="accent-text font-size-20">Health:</span>
33 - <span class="highlight font-size-20">{{ currentIndex.health }}</span>
34 - </div>
35 - </div>
36 - </div>
37 -
38 - <div class="spacer"></div>
39 -
40 - <div class="card-base card-shadow--medium scrollable only-x bg-black">
41 - <el-row class="mt-0" :gutter="30">
42 - <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
43 - <div class="page-table scrollable only-y" id="affix-container">
44 - <div class="page-header">
45 - <h1 class="warning-text">Index Data</h1>
46 - </div>
47 -
48 - <div class="table-box card-base card-shadow--medium scrollable only-x">
49 - <table class="styled striped">
50 - <thead>
51 - <tr>
52 - <th scope="col">Index Name</th>
53 - <th scope="col">Index Health</th>
54 - <th scope="col">Index Document Size</th>
55 - <th scope="col">Storage Size</th>
56 - <th scope="col">Replica Count</th>
57 - </tr>
58 - </thead>
59 - <tr v-for="index in filteredIndices" :key="index.id" :class="{ health: index.health }">
60 - <!-- Display the connector details in the table -->
61 - <td>{{ index.index }}</td>
62 - <td>{{ index.health }}</td>
63 - <td>{{ index.docs_count }}</td>
64 - <td>{{ index.store_size }}</td>
65 - <td>{{ index.replica_count }}</td>
66 -
67 - <!-- Add a buttton to Rotate an Index -->
68 - <td>
69 - <div class="btn-group" role="group" aria-label="Basic example">
70 - <button type="button" class="btn btn-info btn-sm">Rotate Index</button>
71 - </div>
72 - </td>
73 - <!-- Add a buttton to Delete an Index -->
74 - <td>
75 - <div class="btn-group" role="group" aria-label="Basic example">
76 - <button type="button" class="btn btn-info btn-sm" @click="deleteIndex(index.index)">
77 - Delete Index
78 - </button>
79 - </div>
80 - </td>
81 - </tr>
82 - </table>
83 - </div>
84 - </div>
85 - </el-col>
86 - </el-row>
87 -
88 - <!-- New table to display shards -->
89 - <el-row class="mt-0" :gutter="30">
90 - <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
91 - <div class="page-table scrollable only-y" id="affix-container">
92 - <div class="page-header">
93 - <h1 class="warning-text">Index Shards</h1>
94 - </div>
95 -
96 - <div class="table-box card-base card-shadow--medium scrollable only-x">
97 - <table class="styled striped">
98 - <thead>
99 - <tr>
100 - <th scope="col">Shard Index</th>
101 - <th scope="col">Shard ID</th>
102 - <th scope="col">Shard State</th>
103 - <th scope="col">Shard Size</th>
104 - <th scope="col">Shard Node</th>
105 - </tr>
106 - </thead>
107 - <tr v-for="shard in filteredShards" :key="shard.id" class="bg-accent">
108 - <!-- Display the shard details in the table -->
109 - <td>{{ shard.index }}</td>
110 - <td>{{ shard.shard }}</td>
111 - <td>{{ shard.state }}</td>
112 - <td>{{ shard.size }}</td>
113 - <td>{{ shard.node }}</td>
114 - </tr>
115 - </table>
116 - </div>
117 - </div>
118 - </el-col>
119 - </el-row>
120 - </div>
121 -
122 - <div class="el-col el-col-24 el-col-xs-24 el-col-sm-12 el-col-md-12 el-col-lg-16 el-col-xl-16">
123 - <el-row class="chart-row">
124 - <el-col :xs="24" :sm="12" :md="12" :lg="16" :xl="16" class="chart-col">
125 - <div class="chart-container">
126 - <div id="chart" class="chart" :style="{ height: '500px', width: '120%' }"></div>
127 - </div>
128 - </el-col>
129 - <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="8" class="chart-col">
130 - <div class="chart-container">
131 - <div id="pie" class="chart pie-chart" :style="{ height: '500px', width: '200%' }"></div>
132 - </div>
133 - </el-col>
134 - </el-row>
135 - </div>
136 -
137 - <div class="el-col el-col-24 el-col-xs-24 el-col-sm-12 el-col-md-12 el-col-lg-8 el-col-xl-8 flex box grow">
138 - <!-- New table to display shards -->
139 - <el-row class="mt-0" :gutter="30">
140 - <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
141 - <ClusterHealth />
142 - </el-col>
143 -
144 - <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
145 - <div class="page-table scrollable only-y" id="affix-container">
146 - <div class="page-header">
147 - <h1 class="warning-text">Unhealthy Indices</h1>
148 - </div>
149 -
150 - <div class="table-box card-base card-shadow--medium scrollable only-x">
151 - <table class="styled striped hover">
152 - <thead>
153 - <tr>
154 - <th scope="col">Index Name</th>
155 - <th scope="col">Index Health</th>
156 - </tr>
157 - </thead>
158 - <tbody>
159 - <tr
160 - v-for="index in unhealthyIndices"
161 - :key="index.id"
162 - :class="{
163 - 'bg-orange': index.health === 'yellow',
164 - 'bg-red': index.health === 'red'
165 - }"
166 - >
167 - <!-- Display the shard details in the table -->
168 - <td>{{ index.index }}</td>
169 -
170 - <td>{{ index.health }}</td>
171 - </tr>
172 - </tbody>
173 - </table>
174 - </div>
175 - </div>
176 - </el-col>
177 - </el-row>
178 - </div>
179 - </div>
180 - <!--END TEST-->
181 - </el-scrollbar>
182 -</template>
183 -
184 -<script lang="ts">
185 -import * as echarts from "echarts"
186 -import { Index, IndexAllocation, IndexHealth, IndexShard } from "@/types/indices.d"
187 -import Api from "@/api"
188 -import { ElMessage } from "element-plus"
189 -import { defineComponent } from "vue"
190 -import IndicesMarquee from "@/components/indices/Marquee.vue"
191 -import ClusterHealth from "@/components/indices/ClusterHealth.vue"
192 -
193 -export default defineComponent({
194 - data() {
195 - return {
196 - indices: [] as Index[],
197 - shards: [] as IndexShard[],
198 - indicesAllocation: [] as IndexAllocation[],
199 - loadingIndex: false,
200 - loadingShards: false,
201 - loadingAllocation: false,
202 - loadingDeleteIndex: false,
203 - currentIndex: null as Index | null,
204 - selectedValue: "",
205 - selectedHealth: ""
206 - }
207 - },
208 - computed: {
209 - filteredIndices() {
210 - return this.indices.filter((index: Index) => index.index === this.currentIndex?.index)
211 - },
212 - filteredShards() {
213 - return this.shards.filter((shard: IndexShard) => shard.index === this.currentIndex?.index)
214 - },
215 - unhealthyIndices() {
216 - return this.indices.filter((index: Index) => index.health === IndexHealth.YELLOW || index.health === IndexHealth.RED)
217 - },
218 - loading() {
219 - return this.loadingIndex || this.loadingShards || this.loadingAllocation
220 - }
221 - },
222 - methods: {
223 - setIndex(index: Index) {
224 - this.currentIndex = index
225 - },
226 - initChart() {
227 - // Store the indicesAllocation
228 - const indicesAllocation = this.indicesAllocation
229 - // For the indicesAllocation, get the disk_used, disk_total, and timestamp for all items in the array
230 - let data = indicesAllocation.map(item => {
231 - const date = new Date(item.timestamp)
232 - const hours = date.getHours()
233 - const minutes = date.getMinutes()
234 - return {
235 - diskUsed: item.disk_used,
236 - diskTotal: item.disk_total,
237 - timestamp: `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`
238 - }
239 - })
240 -
241 - // Sort data array by timestamp
242 - data.sort((a, b) => {
243 - const aParts = a.timestamp.split(":").map(Number)
244 - const bParts = b.timestamp.split(":").map(Number)
245 - const aDate = new Date(1970, 0, 1, aParts[0], aParts[1])
246 - const bDate = new Date(1970, 0, 1, bParts[0], bParts[1])
247 - return aDate - bDate
248 - })
249 -
250 - // Get the last 12 data points
251 - data = data.slice(Math.max(data.length - 12, 0))
252 -
253 - // Separate the data array into individual arrays for diskUsed, diskTotal, and timestamp
254 - const diskUsed = data.map(item => item.diskUsed)
255 - const diskTotal = data.map(item => item.diskTotal)
256 - const timestamp = data.map(item => item.timestamp)
257 -
258 - console.log("diskUsed: ", diskUsed)
259 - console.log("diskTotal: ", diskTotal)
260 - console.log("timestamp: ", timestamp)
261 -
262 - // Initialize the chart
263 - this.chart = echarts.init(document.getElementById("chart"))
264 - this.chart.setOption({
265 - //backgroundColor: '#394056',
266 - title: {
267 - top: 20,
268 - text: "Wazuh-Indexer Disk Usage",
269 - textStyle: { fontWeight: "normal", fontSize: 16, fontFamily: "Nunito Sans" /*color: '#F1F1F3'*/ },
270 - left: "1%"
271 - },
272 - tooltip: {
273 - trigger: "axis",
274 - axisPointer: {
275 - lineStyle: {
276 - /*color: '#57617B'*/
277 - }
278 - }
279 - },
280 - legend: {
281 - top: 40,
282 - icon: "rect",
283 - itemWidth: 14,
284 - itemHeight: 5,
285 - itemGap: 13,
286 - data: ["Product-A", "Product-B"],
287 - right: "4%",
288 - textStyle: { fontSize: 12, fontFamily: "Nunito Sans" /*color: '#F1F1F3'*/ }
289 - },
290 - grid: {
291 - top: 100,
292 - left: "-5px",
293 - right: "30px",
294 - bottom: "2%",
295 - containLabel: true
296 - },
297 - xAxis: [
298 - {
299 - type: "category",
300 - boundaryGap: false,
301 - axisLine: {
302 - lineStyle: {
303 - /*color: '#57617B'*/
304 - }
305 - },
306 - data: timestamp //timestamp is the x-axis
307 - }
308 - ],
309 - yAxis: [
310 - {
311 - show: false,
312 - type: "value",
313 - name: "(%)",
314 - axisTick: { show: false },
315 - axisLine: {
316 - lineStyle: {
317 - /*color: '#57617B'*/
318 - }
319 - },
320 - axisLabel: {
321 - margin: 10,
322 - fontSize: 14
323 - },
324 - splitLine: { lineStyle: { color: "#eee" /*color: '#57617B'*/ } }
325 - }
326 - ],
327 - series: [
328 - {
329 - name: "Disk Used",
330 - type: "line",
331 - smooth: true,
332 - symbol: "circle",
333 - symbolSize: 5,
334 - showSymbol: false,
335 - lineStyle: { width: 1 },
336 - areaStyle: {
337 - color: new echarts.graphic.LinearGradient(
338 - 0,
339 - 0,
340 - 0,
341 - 1,
342 - [
343 - {
344 - offset: 0,
345 - color: "rgba(19, 206, 102, 0.3)"
346 - },
347 - {
348 - offset: 0.8,
349 - color: "rgba(19, 206, 102, 0)"
350 - }
351 - ],
352 - false
353 - ),
354 - shadowColor: "rgba(0, 0, 0, 0.1)",
355 - shadowBlur: 10
356 - },
357 - itemStyle: {
358 - color: "rgb(19, 206, 102)",
359 - borderColor: "rgba(19, 206, 102, 0.27)",
360 - borderWidth: 12
361 - },
362 - data: diskUsed
363 - },
364 - {
365 - name: "Disk Total",
366 - type: "line",
367 - smooth: true,
368 - symbol: "circle",
369 - symbolSize: 5,
370 - showSymbol: false,
371 - lineStyle: { width: 1 },
372 - areaStyle: {
373 - color: new echarts.graphic.LinearGradient(
374 - 0,
375 - 0,
376 - 0,
377 - 1,
378 - [
379 - {
380 - offset: 0,
381 - color: "rgba(95, 143, 223, 0.3)"
382 - },
383 - {
384 - offset: 0.8,
385 - color: "rgba(95, 143, 223, 0)"
386 - }
387 - ],
388 - false
389 - ),
390 - shadowColor: "rgba(0, 0, 0, 0.1)",
391 - shadowBlur: 10
392 - },
393 - itemStyle: {
394 - color: "rgb(95, 143, 223)",
395 - borderColor: "rgba(95, 143, 223, 0.2)",
396 - borderWidth: 12
397 - },
398 - data: diskTotal
399 - } /*{
400 - name: 'Product-C',
401 - type: 'line',
402 - smooth: true,
403 - symbol: 'circle',
404 - symbolSize: 5,
405 - showSymbol: false,
406 - lineStyle: { width: 1 },
407 - areaStyle: {
408 -
409 - color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
410 - offset: 0,
411 - color: 'rgba(236, 32, 95, 0.3)'
412 - }, {
413 - offset: 0.8,
414 - color: 'rgba(236, 32, 95, 0)'
415 - }], false),
416 - shadowColor: 'rgba(0, 0, 0, 0.1)',
417 - shadowBlur: 10
418 -
419 - },
420 - itemStyle: {
421 -
422 - color: 'rgb(236, 32, 95)',
423 - borderColor: 'rgba(236, 32, 95, 0.2)',
424 - borderWidth: 12
425 -
426 - },
427 - data: [220, 182, 125, 145, 122, 191, 134, 150, 120, 110, 165, 122]
428 - }*/
429 - ]
430 - })
431 - },
432 - initPie() {
433 - // const topIndexes = this.indices.sort((a, b) => b.store_size - a.store_size).slice(0, 5)
434 - const topIndexes = this.indices.sort((a, b) => b.store_size - a.store_size).slice(0, 8)
435 -
436 - const size = topIndexes.map(index => {
437 - const value = parseFloat(index.store_size) / 1024 // Convert MB to GB
438 - return {
439 - value: value,
440 - name: index.index,
441 - health: index.health,
442 - itemStyle: {
443 - color: "#3f84f6" // Custom color for each index
444 - }
445 - }
446 - })
447 - console.log("Size", size)
448 -
449 - const data = topIndexes.map(index => {
450 - // const value = parseFloat(index.store_size) // Parse the value as a float and remvoe the decimal
451 - const value = parseFloat(index.store_size) / 1000000000 // Parse the value as a float and convert to GB
452 - return {
453 - value: value,
454 - name: index.index,
455 - health: index.health,
456 - itemStyle: {
457 - color: "#3f84f6" // Custom color for each index
458 - }
459 - }
460 - })
461 - console.log(data)
462 -
463 - const redIndices = data.filter(index => index.health === "red")
464 - const yellowIndices = data.filter(index => index.health === "yellow")
465 - const greenIndices = data.filter(index => index.health === "green")
466 -
467 - // Get the percentage of green indices
468 - const greenPercentage = (greenIndices.length / data.length) * 100
469 - const yellowPercentage = (yellowIndices.length / data.length) * 100
470 - const redPercentage = (redIndices.length / data.length) * 100
471 - console.log("Red Indices:", redIndices)
472 - console.log("Yellow Indices:", yellowIndices)
473 - console.log("Green Indices:", greenIndices)
474 - console.log("Green Indices Percentage:", greenPercentage)
475 -
476 - // const ordersValue = data.length > 0 ? data[0].value.toFixed(2) : 0 // Get the rounded value of the first item in the data array
477 -
478 - this.pie = echarts.init(document.getElementById("pie"))
479 - this.pie.setOption({
480 - title: {
481 - top: 20,
482 - text: "Index Status and Top 8 Index Sizes By GB",
483 - textStyle: { fontWeight: "normal", fontSize: 16, fontFamily: "Nunito Sans" /*color: '#F1F1F3'*/ },
484 - left: "1%"
485 - },
486 - tooltip: {
487 - trigger: "item",
488 - formatter: "{a} <br/>{b}: {c} ({d}%)"
489 - },
490 - series: [
491 - {
492 - name: "Index",
493 - type: "pie",
494 - selectedMode: "single",
495 - radius: [0, "35%"],
496 -
497 - label: {
498 - position: "inner"
499 - },
500 - labelLine: {
501 - show: false
502 - },
503 - data: [
504 - {
505 - // set the value as the index size
506 - value: greenPercentage.toFixed(2),
507 - name: "Green Indices",
508 - selected: true,
509 - itemStyle: { color: "rgb(19, 206, 102)" }
510 - },
511 - {
512 - value: yellowPercentage.toFixed(2),
513 - name: "Yellow Indices",
514 - itemStyle: { color: "rgb(255, 255, 0)" }
515 - },
516 - {
517 - value: redPercentage.toFixed(2),
518 - name: "Red Indices",
519 - itemStyle: { color: "rgb(255, 0, 0)" }
520 - }
521 - ]
522 - },
523 - {
524 - name: "Index",
525 - type: "pie",
526 - radius: ["45%", "60%"],
527 - data: size.map((item, index) => ({
528 - value: item.value.toFixed(2),
529 - name: item.name,
530 - itemStyle: {
531 - color: item.itemStyle.color
532 - }
533 - })),
534 -
535 - itemStyle: {
536 - color: "rgb(19, 206, 102)"
537 - }
538 - }
539 - ]
540 - })
541 - },
542 - deleteIndex(index) {
543 - this.loadingDeleteIndex = true
544 -
545 - Api.indices
546 - .deleteIndex(index)
547 - .then(res => {
548 - ElMessage({
549 - message: "Index was successfully deleted.",
550 - type: "success"
551 - })
552 -
553 - this.getIndices()
554 - })
555 - .catch(err => {
556 - if (err.response.status === 401) {
557 - ElMessage({
558 - message: "Wazuh-Indexer returned Unauthorized. Please check your connector credentials.",
559 - type: "error"
560 - })
561 - } else if (err.response.status === 404) {
562 - ElMessage({
563 - message: err.response?.data?.message || "An error occurred. Please try again later.",
564 - type: "error"
565 - })
566 - } else {
567 - ElMessage({
568 - message: "An error occurred. Please try again later.",
569 - type: "error"
570 - })
571 - }
572 - })
573 - .finally(() => {
574 - this.loadingDeleteIndex = false
575 - })
576 - },
577 - getIndicesAllocation() {
578 - this.loadingAllocation = true
579 - Api.indices
580 - .getAllocation()
581 - .then(res => {
582 - this.indicesAllocation = res.data.node_allocation
583 - this.initChart()
584 - })
585 - .catch(err => {
586 - if (err.response.status === 401) {
587 - ElMessage({
588 - message: "Wazuh-Indexer returned Unauthorized. Please check your connector credentials.",
589 - type: "error"
590 - })
591 - } else if (err.response.status === 404) {
592 - ElMessage({
593 - message: "No alerts were found.",
594 - type: "error"
595 - })
596 - } else {
597 - ElMessage({
598 - message: "An error occurred. Please try again later.",
599 - type: "error"
600 - })
601 - }
602 - })
603 - .finally(() => {
604 - this.loadingAllocation = false
605 - })
606 - },
607 - getIndices() {
608 - this.loadingIndex = true
609 - Api.indices
610 - .getIndices()
611 - .then(res => {
612 - this.indices = res.data.indices
613 - this.initPie()
614 - })
615 - .catch(err => {
616 - if (err.response.status === 401) {
617 - ElMessage({
618 - message: "Wazuh-Indexer returned Unauthorized. Please check your connector credentials.",
619 - type: "error"
620 - })
621 - } else if (err.response.status === 404) {
622 - ElMessage({
623 - message: "No alerts were found.",
624 - type: "error"
625 - })
626 - } else {
627 - ElMessage({
628 - message: "An error occurred. Please try again later.",
629 - type: "error"
630 - })
631 - }
632 - })
633 - .finally(() => {
634 - this.loadingIndex = false
635 - })
636 - },
637 - getShards() {
638 - this.loadingShards = true
639 - Api.indices
640 - .getShards()
641 - .then(res => {
642 - this.shards = res.data.shards
643 - })
644 - .catch(err => {
645 - if (err.response.status === 401) {
646 - ElMessage({
647 - message: "Wazuh-Indexer returned Unauthorized. Please check your connector credentials.",
648 - type: "error"
649 - })
650 - } else if (err.response.status === 404) {
651 - ElMessage({
652 - message: "No alerts were found.",
653 - type: "error"
654 - })
655 - } else {
656 - ElMessage({
657 - message: "An error occurred. Please try again later.",
658 - type: "error"
659 - })
660 - }
661 - })
662 - .finally(() => {
663 - this.loadingShards = false
664 - })
665 - },
666 -
667 - getPieChartData() {
668 - const topIndexes = this.indices.sort((a, b) => b.store_size - a.store_size).slice(0, 8)
669 -
670 - return topIndexes.map((index, i) => ({
671 - value: index.store_size,
672 - name: `p${i + 1}`,
673 - itemStyle: {
674 - color: "#3f84f6" // Custom color for each index
675 - }
676 - }))
677 - }
678 - },
679 - beforeUnmount() {
680 - this.pie?.dispose()
681 - this.chart?.dispose()
682 - },
683 - created() {
684 - this.getIndices()
685 - this.getShards()
686 - this.getIndicesAllocation()
687 - },
688 - components: { IndicesMarquee, ClusterHealth }
689 -})
690 -</script>
691 -
692 -<style lang="scss" scoped>
693 -@import "../../../assets/scss/_variables";
694 -</style>