Skip to content

Commit 5c5b83d

Browse files
roshanmaskeyjkpprberggren
authored
Adding an AnalyzerOutput class (#2706)
* Adding a class for a more structured way of analyzer outputs. * Validates output via jsonschema * returns a json string for the database --------- Co-authored-by: janosch <jkpr@google.com> Co-authored-by: Johan Berggren <jberggren@gmail.com> Co-authored-by: Janosch <99879757+jkppr@users.noreply.github.com>
1 parent 63afb2d commit 5c5b83d

File tree

1 file changed

+219
-0
lines changed

1 file changed

+219
-0
lines changed

timesketch/lib/analyzers/interface.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,13 @@
2222
import random
2323
import time
2424
import traceback
25+
26+
2527
import yaml
2628

2729
import opensearchpy
2830
from flask import current_app
31+
from jsonschema import validate, ValidationError, SchemaError
2932

3033
import pandas
3134

@@ -884,6 +887,18 @@ def __init__(self, index_name, sketch_id, timeline_id=None):
884887
port=current_app.config["OPENSEARCH_PORT"],
885888
)
886889

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+
887902
if not hasattr(self, "sketch"):
888903
self.sketch = None
889904

@@ -1150,3 +1165,207 @@ def get_kwargs(cls):
11501165
def run(self):
11511166
"""Entry point for the analyzer."""
11521167
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

Comments
 (0)