Skip to content
Open
47 changes: 35 additions & 12 deletions source/app/blueprints/pages/case/templates/case_graph_timeline.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,9 @@
<div class="main-panel">
<div class="content">
<nav class="navbar navbar-header navbar-expand-lg bg-primary-gradient">
{% set group_by = request.args.get('group-by') %}
{% set include_children = request.args.get('include-children', 'true')|lower in ['1', 'true', 'yes', 'on'] %}
<ul class="container-fluid mt-3 mb--2">
<ul class="navbar-nav">
<li class="nav-item hidden-caret">
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}">No group</a>
</li>
<li class="nav-item hidden-caret">
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}&group-by=asset"><span class="text-decoration-none">Group by asset</span></a>
</li>
<li class="nav-item hidden-caret">
<a class="menu-title btn btn-dark btn-sm" href="visualize?cid={{session['current_case'].case_id}}&group-by=category">Group by category</a>
</li>
</ul>
<ul class="navbar-nav topbar-nav ml-md-auto align-items-center page-navigation page-navigation-style-2 page-navigation-secondary">
<li class="nav-item ml-2">
<span class="text-white text-sm mr-2" id="last_resfresh">Loading</span>
Expand All @@ -41,6 +32,25 @@
<span class="menu-title">Refresh</span>
</button>
</li>
<li class="nav-item">
<div class="dropdown">
<button class="btn btn-sm btn-border btn-black" id="dropdownVisualizationMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="menu-title"><i class="fas fa-ellipsis-v"></i></span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownVisualizationMenuButton">
<a class="dropdown-item" href="visualize?cid={{session['current_case'].case_id}}&include-children={{ 'true' if include_children else 'false' }}">No group</a>
<a class="dropdown-item" href="visualize?cid={{session['current_case'].case_id}}&group-by=ioc&include-children={{ 'true' if include_children else 'false' }}">Group by IOC</a>
<a class="dropdown-item" href="visualize?cid={{session['current_case'].case_id}}&group-by=asset&include-children={{ 'true' if include_children else 'false' }}">Group by asset</a>
<a class="dropdown-item" href="visualize?cid={{session['current_case'].case_id}}&group-by=color&include-children={{ 'true' if include_children else 'false' }}">Group by color</a>
<a class="dropdown-item" href="visualize?cid={{session['current_case'].case_id}}&group-by=tag&include-children={{ 'true' if include_children else 'false' }}">Group by tag</a>
<a class="dropdown-item" href="visualize?cid={{session['current_case'].case_id}}&group-by=category&include-children={{ 'true' if include_children else 'false' }}">Group by category</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="visualize?cid={{session['current_case'].case_id}}{% if group_by %}&group-by={{ group_by }}{% endif %}&include-children={{ 'false' if include_children else 'true' }}">Toggle children ({{ 'On' if include_children else 'Off' }})</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" onclick="reset_timeline_graph(); return false;">Reset visualization</a>
</div>
</div>
</li>
</ul>
</ul>
</nav>
Expand All @@ -58,6 +68,18 @@
</div>
</div>
</div>

<div class="modal shadow-lg" tabindex="-1" id="modal_add_event" data-backdrop="static">
<div class="modal-xl modal-dialog" role="document">
<div class="modal-content" id="modal_add_event_content"></div>
</div>
</div>

<div class="modal" role="dialog" tabindex="-1" id="modal_comment" data-backdrop="false">
<div class="modal-lg modal-dialog modal-comment" role="document">
<div class="modal-content shadow-xl" id="modal_comment_content"></div>
</div>
</div>
</div>
{% include 'includes/footer.html' %}
</div>
Expand All @@ -73,6 +95,7 @@
<script src="/static/assets/js/plugin/select/bootstrap-select.min.js"></script>

<script src="/static/assets/js/iris/case.common.js"></script>
<script src="/static/assets/js/iris/comments.js"></script>
<script src="/static/assets/js/iris/case.timeline.visu.js"></script>
<script src="/static/assets/js/plugin/vis/vis.graph.js"></script>
<script>
Expand All @@ -82,4 +105,4 @@
});
</script>

{% endblock javascripts %}
{% endblock javascripts %}
11 changes: 7 additions & 4 deletions source/app/blueprints/pages/case/templates/case_timeline.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@
</button>
</li>
<li class="nav-item hidden-caret">
<button class="btn btn-dark btn-sm" onclick="toggle_tree_view();">
<span class="menu-title">Toggle view</span>
</button>
<a class="btn btn-dark btn-sm" href="timeline/visualize?cid={{session['current_case'].case_id}}">
<span class="menu-title">Visualize</span>
</a>
</li>
<li class="nav-item">
<div class="dropdown">
Expand All @@ -67,7 +67,10 @@
<a class="dropdown-item" href="javascript:void(0);" onclick="toggle_compact_view();"> Toggle compact view</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}"> Visualize</a>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=ioc"> Visualize by IOC</a>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=asset"> Visualize by asset</a>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=color"> Visualize by color</a>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=tag"> Visualize by tag</a>
<a class="dropdown-item" href="timeline/visualize?cid={{session['current_case'].case_id}}&group-by=category">Visualize by category</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" onclick="timelineToCsv();"><small class="fa fa-download mr-2"></small> Download as CSV</a>
Expand Down Expand Up @@ -183,4 +186,4 @@ <h5>Upload events list (CSV format)</h5>
<script src="/static/assets/js/core/socket.io.js"></script>
<script src="/static/assets/js/timeline.js"></script>

{% endblock javascripts %}
{% endblock javascripts %}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ <h4 class="modal-title mr-4">{% if event.event_id %} Event ID #{{ event.event_i
<div class="form-check">
<label class="form-check-label mt-3">
{{ form.event_in_summary(class="form-check-input", type="checkbox") }}
<span class="form-check-sign"> Add to summary
<span class="form-check-sign"> Add to visualization
<i class="ml-1 mt-1 fa-regular fa-circle-question" title="If checked, the event will be integrated in the Timeline Visualization" style="cursor:pointer;"></i>
</span>
</label>
Expand Down Expand Up @@ -326,4 +326,4 @@ <h4 class="modal-title mr-4">{% if event.event_id %} Event ID #{{ event.event_i
]);
$('#event_iocs').trigger('change');
</script>
{% endif %}
{% endif %}
176 changes: 153 additions & 23 deletions source/app/blueprints/rest/case/case_timeline_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,25 +174,129 @@ def case_get_timeline_state(caseid):
return response_error('No timeline state for this case. Add an event to begin')


def _get_visualization_event(row, group_name):
content = row.event_content.replace('\n', '<br/>') if row.event_content else ''
styles = []

if row.event_color:
styles.append(f'background-color: {row.event_color};')

# Highlight child events in visualization with a dashed outline.
if row.parent_event_id is not None:
styles.append('border: 1px dashed #6c757d;')
styles.append('box-sizing: border-box;')

visualized_event = {
'date': row.event_date,
'group': group_name,
'content': row.event_title,
'title': f"<small>{row.event_date.strftime('%Y-%m-%dT%H:%M:%S')}</small><br/>{content}",
'unique_id': row.event_id
}

if styles:
visualized_event['style'] = ' '.join(styles)

return visualized_event


def _expand_timeline_by_group(timeline, events_groups, fallback_group):
tim = []
for row in timeline:
grouped_values = events_groups.get(row.event_id, []) or [fallback_group]
for group_name in grouped_values:
tim.append(_get_visualization_event(row, group_name))

return tim


def _build_events_groups(rows, value_key):
events_groups = {}
for row in rows:
event_id = row.event_id
group_value = getattr(row, value_key, None)
if not group_value:
continue

events_groups.setdefault(event_id, [])
if group_value not in events_groups[event_id]:
events_groups[event_id].append(group_value)

return events_groups


def _get_event_color_group_name(event_color):
event_color_map = {
'#fff': 'White',
'#1572e899': 'Blue',
'#6861ce99': 'Purple',
'#48abf799': 'Light blue',
'#31ce3699': 'Green',
'#f2596199': 'Red',
'#ffad4699': 'Orange'
}

if not event_color:
return 'No color'

return event_color_map.get(event_color.lower(), event_color)


def _is_include_children():
include_children = request.args.get('include-children')
if include_children is None:
return True

normalized_value = str(include_children).strip().lower()
if '?' in normalized_value:
normalized_value = normalized_value.split('?', maxsplit=1)[0]
if '&' in normalized_value:
normalized_value = normalized_value.split('&', maxsplit=1)[0]

return normalized_value in ('1', 'true', 'yes', 'on')


def _get_visualization_timeline(caseid):
timeline = get_events_by_case(caseid)
if _is_include_children():
return timeline

return [row for row in timeline if row.parent_event_id is None]


@case_timeline_rest_blueprint.route('/case/timeline/visualize/data/by-asset', methods=['GET'])
@ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
@ac_api_requires()
def case_getgraph_assets(caseid):
timeline = _get_visualization_timeline(caseid)
assets_cache = get_assets_by_case(caseid)
timeline = get_events_by_case(caseid)
events_assets = _build_events_groups(assets_cache, 'asset_name')

tim = []
for row in timeline:
for asset in assets_cache:
if asset.event_id == row.event_id:
tmp = {'date': row.event_date, 'group': asset.asset_name, 'content': row.event_title,
'title': f"{row.event_date.strftime('%Y-%m-%dT%H:%M:%S')} - {row.event_content}"}
tim = _expand_timeline_by_group(timeline, events_assets, 'No linked assets')

if row.event_color:
tmp['style'] = f'background-color: {row.event_color};'
res = {
"events": tim
}

return response_success("", data=res)

tmp['unique_id'] = row.event_id
tim.append(tmp)

@case_timeline_rest_blueprint.route('/case/timeline/visualize/data/by-ioc', methods=['GET'])
@ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
@ac_api_requires()
def case_getgraph_iocs(caseid):
timeline = _get_visualization_timeline(caseid)
iocs_cache = CaseEventsIoc.query.with_entities(
CaseEventsIoc.event_id,
Ioc.ioc_value
).join(
CaseEventsIoc.ioc
).filter(
CaseEventsIoc.case_id == caseid
).all()
events_iocs = _build_events_groups(iocs_cache, 'ioc_value')

tim = _expand_timeline_by_group(timeline, events_iocs, 'No linked IOCs')

res = {
"events": tim
Expand All @@ -205,25 +309,51 @@ def case_getgraph_assets(caseid):
@ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
@ac_api_requires()
def case_getgraph(caseid):
timeline = get_events_by_case(caseid)
timeline = _get_visualization_timeline(caseid)
events_categories = {}
for row in timeline:
group_name = row.category[0].name if row.category else 'Uncategorized'
events_categories[row.event_id] = [group_name]

tim = []
tim = _expand_timeline_by_group(timeline, events_categories, 'Uncategorized')

res = {
"events": tim
}

return response_success("", data=res)


@case_timeline_rest_blueprint.route('/case/timeline/visualize/data/by-tag', methods=['GET'])
@ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
@ac_api_requires()
def case_getgraph_tags(caseid):
timeline = _get_visualization_timeline(caseid)
events_tags = {}
for row in timeline:
tags = [tag.strip() for tag in (row.event_tags or '').split(',') if tag.strip()]
events_tags[row.event_id] = list(dict.fromkeys(tags))

tmp = {'date': row.event_date, 'group': row.category[0].name if row.category else 'Uncategorized', 'content': row.event_title}
tim = _expand_timeline_by_group(timeline, events_tags, 'No tags')

if row.event_content:
content = row.event_content.replace('\n', '<br/>')
else:
content = ''
res = {
"events": tim
}

return response_success("", data=res)

tmp['title'] = f"<small>{row.event_date.strftime('%Y-%m-%dT%H:%M:%S')}</small><br/>{content}"

if row.event_color:
tmp['style'] = f'background-color: {row.event_color};'
@case_timeline_rest_blueprint.route('/case/timeline/visualize/data/by-color', methods=['GET'])
@ac_requires_case_identifier(CaseAccessLevel.read_only, CaseAccessLevel.full_access)
@ac_api_requires()
def case_getgraph_colors(caseid):
timeline = _get_visualization_timeline(caseid)
events_colors = {}
for row in timeline:
color_name = _get_event_color_group_name(row.event_color)
events_colors[row.event_id] = [color_name]

tmp['unique_id'] = row.event_id
tim.append(tmp)
tim = _expand_timeline_by_group(timeline, events_colors, 'No color')

res = {
"events": tim
Expand Down
2 changes: 1 addition & 1 deletion source/app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class CaseEventForm(FlaskForm):
event_assets = SelectField(u'Event Asset')
event_category_id = SelectField(u'Event Category')
event_tz = StringField(u'Event Timezone', validators=[DataRequired()])
event_in_summary = BooleanField(u'Add to summary')
event_in_summary = BooleanField(u'Add to visualization')
event_tags = StringField(u'Event Tags')
event_in_graph = BooleanField(u'Display in graph')

Expand Down
Loading