|
22 | 22 | import random
|
23 | 23 | import time
|
24 | 24 | import traceback
|
| 25 | + |
| 26 | + |
25 | 27 | import yaml
|
26 | 28 |
|
27 | 29 | import opensearchpy
|
28 | 30 | from flask import current_app
|
| 31 | +from jsonschema import validate, ValidationError, SchemaError |
29 | 32 |
|
30 | 33 | import pandas
|
31 | 34 |
|
@@ -884,6 +887,18 @@ def __init__(self, index_name, sketch_id, timeline_id=None):
|
884 | 887 | port=current_app.config["OPENSEARCH_PORT"],
|
885 | 888 | )
|
886 | 889 |
|
| 890 | + # Add AnalyzerOutput instance and set all attributes that can be set |
| 891 | + # automatically |
| 892 | + self.output = AnalyzerOutput( |
| 893 | + analyzer_identifier=self.NAME, |
| 894 | + analyzer_name=self.DISPLAY_NAME, |
| 895 | + timesketch_instance=current_app.config.get( |
| 896 | + "EXTERNAL_HOST_URL", "https://localhost" |
| 897 | + ), |
| 898 | + sketch_id=sketch_id, |
| 899 | + timeline_id=timeline_id, |
| 900 | + ) |
| 901 | + |
887 | 902 | if not hasattr(self, "sketch"):
|
888 | 903 | self.sketch = None
|
889 | 904 |
|
@@ -1150,3 +1165,207 @@ def get_kwargs(cls):
|
1150 | 1165 | def run(self):
|
1151 | 1166 | """Entry point for the analyzer."""
|
1152 | 1167 | raise NotImplementedError
|
| 1168 | + |
| 1169 | + |
| 1170 | +class AnalyzerOutputException(Exception): |
| 1171 | + """Analyzer output exception.""" |
| 1172 | + |
| 1173 | + |
| 1174 | +class AnalyzerOutput: |
| 1175 | + """A class to record timesketch analyzer output. |
| 1176 | +
|
| 1177 | + Attributes: |
| 1178 | + platform (str): [Required] Analyzer platfrom. |
| 1179 | + analyzer_identifier (str): [Required] Unique analyzer identifier. |
| 1180 | + analyzer_name (str): [Required] Analyzer display name. |
| 1181 | + result_status (str): [Required] Analyzer result status. |
| 1182 | + Valid values are success or error. |
| 1183 | + result_priority (str): [Required] Priority of the result based on the |
| 1184 | + analysis findings. Valid values are NOTE (default), LOW, MEDIUM, HIGH. |
| 1185 | + result_summary (str): [Required] A summary statement of the analyzer |
| 1186 | + finding. A result summary must exist even if there is no finding. |
| 1187 | + result_markdown (str): [Optional] A detailed information about the |
| 1188 | + analyzer finding in a markdown format. |
| 1189 | + references (List[str]): [Optional] A list of references about the |
| 1190 | + analyzer or the issue the analyzer attempts to address. |
| 1191 | + result_attributes (dict): [Optional] A dict of key : value pairs that |
| 1192 | + holds additional finding details. |
| 1193 | + platform_meta_data: (dict): [Required] A dict of key : value pairs that |
| 1194 | + holds the following information: |
| 1195 | + timesketch_instance (str): [Required] The Timesketch instance URL. |
| 1196 | + sketch_id (int): [Required] Timesketch sketch ID for this analyzer. |
| 1197 | + timeline_id (int): [Required] Timesketch timeline ID for this analyzer. |
| 1198 | + saved_views (List[int]): [Optional] Views generatred by the analyzer. |
| 1199 | + saved_stories (List[int]): [Optional] Stories generated by the analyzer. |
| 1200 | + saved_graphs (List[int]): [Optional] Graphs generated by the analyzer. |
| 1201 | + saved_aggregations (List[int]): [Optional] Aggregations generated |
| 1202 | + by the analyzer. |
| 1203 | + created_tags (List[str]): [Optional] Tags created by the analyzer. |
| 1204 | + """ |
| 1205 | + |
| 1206 | + def __init__( |
| 1207 | + self, |
| 1208 | + analyzer_identifier, |
| 1209 | + analyzer_name, |
| 1210 | + timesketch_instance, |
| 1211 | + sketch_id, |
| 1212 | + timeline_id, |
| 1213 | + ): |
| 1214 | + """Initialize analyzer output.""" |
| 1215 | + self.platform = "timesketch" |
| 1216 | + self.analyzer_identifier = analyzer_identifier |
| 1217 | + self.analyzer_name = analyzer_name |
| 1218 | + self.result_status = "" # TODO: link to analyzer status/error? |
| 1219 | + self.result_priority = "NOTE" |
| 1220 | + self.result_summary = "" |
| 1221 | + self.result_markdown = "" |
| 1222 | + self.references = [] |
| 1223 | + self.result_attributes = {} |
| 1224 | + self.platform_meta_data = { |
| 1225 | + "timesketch_instance": timesketch_instance, |
| 1226 | + "sketch_id": sketch_id, |
| 1227 | + "timeline_id": timeline_id, |
| 1228 | + "saved_views": [], |
| 1229 | + "saved_stories": [], |
| 1230 | + "saved_graphs": [], |
| 1231 | + "saved_aggregations": [], |
| 1232 | + "created_tags": [], |
| 1233 | + } |
| 1234 | + |
| 1235 | + def validate(self): |
| 1236 | + """Validates the analyzer output and raises exception.""" |
| 1237 | + schema = { |
| 1238 | + "$schema": "http://json-schema.org/draft-07/schema#", |
| 1239 | + "type": "object", |
| 1240 | + "properties": { |
| 1241 | + "platform": {"type": "string", "enum": ["timesketch"]}, |
| 1242 | + "analyzer_identifier": {"type": "string", "minLength": 1}, |
| 1243 | + "analyzer_name": {"type": "string", "minLength": 1}, |
| 1244 | + "result_status": { |
| 1245 | + "type": "string", |
| 1246 | + "enum": ["SUCCESS", "NO-FINDINGS", "ERROR"], |
| 1247 | + }, |
| 1248 | + "result_priority": { |
| 1249 | + "type": "string", |
| 1250 | + "default": "NOTE", |
| 1251 | + "enum": ["HIGH", "MEDIUM", "LOW", "NOTE"], |
| 1252 | + }, |
| 1253 | + "result_summary": {"type": "string", "minLength": 1}, |
| 1254 | + "result_markdown": {"type": "string", "minLength": 1}, |
| 1255 | + "references": { |
| 1256 | + "type": "array", |
| 1257 | + "items": [{"type": "string", "minLength": 1}], |
| 1258 | + }, |
| 1259 | + "result_attributes": {"type": "object"}, |
| 1260 | + "platform_meta_data": { |
| 1261 | + "type": "object", |
| 1262 | + "properties": { |
| 1263 | + "timesketch_instance": {"type": "string", "minLength": 1}, |
| 1264 | + "sketch_id": {"type": "integer"}, |
| 1265 | + "timeline_id": {"type": "integer"}, |
| 1266 | + "saved_views": { |
| 1267 | + "type": "array", |
| 1268 | + "items": [ |
| 1269 | + {"type": "integer"}, |
| 1270 | + ], |
| 1271 | + }, |
| 1272 | + "saved_stories": { |
| 1273 | + "type": "array", |
| 1274 | + "items": [{"type": "integer"}], |
| 1275 | + }, |
| 1276 | + "saved_aggregations": { |
| 1277 | + "type": "array", |
| 1278 | + "items": [ |
| 1279 | + {"type": "integer"}, |
| 1280 | + ], |
| 1281 | + }, |
| 1282 | + "created_tags": { |
| 1283 | + "type": "array", |
| 1284 | + "items": [ |
| 1285 | + {"type": "string"}, |
| 1286 | + ], |
| 1287 | + }, |
| 1288 | + }, |
| 1289 | + "required": [ |
| 1290 | + "timesketch_instance", |
| 1291 | + "sketch_id", |
| 1292 | + "timeline_id", |
| 1293 | + ], |
| 1294 | + }, |
| 1295 | + }, |
| 1296 | + "required": [ |
| 1297 | + "platform", |
| 1298 | + "analyzer_identifier", |
| 1299 | + "analyzer_name", |
| 1300 | + "result_status", |
| 1301 | + "result_priority", |
| 1302 | + "result_summary", |
| 1303 | + "platform_meta_data", |
| 1304 | + ], |
| 1305 | + } |
| 1306 | + |
| 1307 | + try: |
| 1308 | + validate(instance=self.to_json(), schema=schema) |
| 1309 | + return True |
| 1310 | + except (ValidationError, SchemaError) as e: |
| 1311 | + raise AnalyzerOutputException(f"json schema error: {e}") from e |
| 1312 | + |
| 1313 | + def to_json(self) -> dict: |
| 1314 | + """Returns JSON output of AnalyzerOutput. Filters out empty values.""" |
| 1315 | + # add required fields |
| 1316 | + output = { |
| 1317 | + "platform": self.platform, |
| 1318 | + "analyzer_identifier": self.analyzer_identifier, |
| 1319 | + "analyzer_name": self.analyzer_name, |
| 1320 | + "result_status": self.result_status.upper(), |
| 1321 | + "result_priority": self.result_priority.upper(), |
| 1322 | + "result_summary": self.result_summary, |
| 1323 | + "platform_meta_data": { |
| 1324 | + "timesketch_instance": self.platform_meta_data["timesketch_instance"], |
| 1325 | + "sketch_id": self.platform_meta_data["sketch_id"], |
| 1326 | + "timeline_id": self.platform_meta_data["timeline_id"], |
| 1327 | + }, |
| 1328 | + } |
| 1329 | + |
| 1330 | + # add optional fields if they are not empty |
| 1331 | + if self.result_markdown and self.result_markdown != "": |
| 1332 | + output["result_markdown"] = self.result_markdown |
| 1333 | + |
| 1334 | + if self.references: |
| 1335 | + output["references"] = self.references |
| 1336 | + |
| 1337 | + if self.result_attributes: |
| 1338 | + output["result_attributes"] = self.result_attributes |
| 1339 | + |
| 1340 | + if self.platform_meta_data["saved_views"]: |
| 1341 | + output["platform_meta_data"]["saved_views"] = self.platform_meta_data[ |
| 1342 | + "saved_views" |
| 1343 | + ] |
| 1344 | + |
| 1345 | + if self.platform_meta_data["saved_stories"]: |
| 1346 | + output["platform_meta_data"]["saved_stories"] = self.platform_meta_data[ |
| 1347 | + "saved_stories" |
| 1348 | + ] |
| 1349 | + |
| 1350 | + if self.platform_meta_data["saved_graphs"]: |
| 1351 | + output["platform_meta_data"]["saved_graphs"] = self.platform_meta_data[ |
| 1352 | + "saved_graphs" |
| 1353 | + ] |
| 1354 | + |
| 1355 | + if self.platform_meta_data["saved_aggregations"]: |
| 1356 | + output["platform_meta_data"][ |
| 1357 | + "saved_aggregations" |
| 1358 | + ] = self.platform_meta_data["saved_aggregations"] |
| 1359 | + |
| 1360 | + if self.platform_meta_data["created_tags"]: |
| 1361 | + output["platform_meta_data"]["created_tags"] = self.platform_meta_data[ |
| 1362 | + "created_tags" |
| 1363 | + ] |
| 1364 | + |
| 1365 | + return output |
| 1366 | + |
| 1367 | + def __str__(self) -> str: |
| 1368 | + """Returns string output of AnalyzerOutput.""" |
| 1369 | + if self.validate(): |
| 1370 | + return json.dumps(self.to_json()) |
| 1371 | + return "" |
0 commit comments