Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow column manipulation of event table #312

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions html/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@
max-width: 300px;
}

.tiny-icon {
font-size: 14px !important;
}

.stale {
opacity: 0.3;
}
Expand Down
57 changes: 42 additions & 15 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
<v-icon>fa-expand</v-icon>
</router-link>
<v-btn icon class="mx-1" @click="removeGroupBy(groupIdx, -1)" :title="i18n.remove">
<v-icon>fa-trash</v-icon>
<v-icon class="tiny-icon">fa-circle-xmark</v-icon>
</v-btn>
<pie-chart v-if="group.chart_type == 'pie'" :id="'group-' + groupIdx" :chartdata="group.chart_data" :options="group.chart_options"/>
<bar-chart v-if="group.chart_type == 'bar'" :id="'group-' + groupIdx" :chartdata="group.chart_data" :options="group.chart_options"/>
Expand All @@ -554,7 +554,7 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
<v-icon>fa-expand</v-icon>
</router-link>
<v-btn v-if="header.value != 'count' && header.value != ''" icon x-small @click.stop="removeGroupBy(groupIdx,headerIdx - getGroupByFieldStartIndex())" :title="i18n.remove">
<v-icon>fa-trash</v-icon>
<v-icon class="tiny-icon">fa-circle-xmark</v-icon>
</v-btn>
</span>
</template>
Expand Down Expand Up @@ -630,6 +630,26 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
</v-row>
</v-container>
</template>
<template v-for="(header, headerIdx) in eventHeaders" v-slot:["header." + header.value]="{ props }">
<span :id="'col_' + header.value">
<span v-if="header.value != ''" class="table-header-actions">
<v-btn :id="'col_move_left_' + header.value" icon x-small @click.stop="moveColumnHeader(header.value, true)" :title="i18n.moveLeft">
<v-icon class="tiny-icon">fa-caret-left</v-icon>
</v-btn>
</span>
<span :id="'col_label_' + header.value">
{{ header.text }}
</span>
<span v-if="header.value != ''" class="table-header-actions">
<v-btn :id="'col_move_right_' + header.value" icon x-small @click.stop="moveColumnHeader(header.value, false)" :title="i18n.moveRight">
<v-icon class="tiny-icon">fa-caret-right</v-icon>
</v-btn>
<v-btn :id="'col_del_' + header.value" icon x-small @click.stop="removeColumnHeader(header.value)" :title="i18n.remove">
<v-icon class="tiny-icon">fa-circle-xmark</v-icon>
</v-btn>
</span>
</span>
</template>
<template v-slot:item="{ item, index }">
<tr>
<td>
Expand Down Expand Up @@ -667,9 +687,12 @@ <h4 v-if="loaded">{{ i18n.eventTotal }} {{ totalEvents.toLocaleString() }}</h4>
<tr class="pt-3">
<td class="expansion">
<div class="d-inline-block semi-transparent">
<v-btn :disabled="!canQuery(item.key)" icon x-small :to="buildGroupByRoute(item.key)">
<v-btn :id="'groupby_' + item.key" :disabled="!canQuery(item.key)" icon x-small :to="buildGroupByRoute(item.key)">
<v-icon :alt="i18n.groupInclude" :title="i18n.groupIncludeHelp">fa-layer-group</v-icon>
</v-btn>
<v-btn :id="'col_toggle_' + item.key" icon x-small @click="toggleColumnHeader(item.key)">
<v-icon :alt="i18n.addColumnHeader" :color="isColumnHeader(item.key) ? 'info' : ''" :title="i18n.addColumnHeaderHelp">fa-table-columns</v-icon>
</v-btn>
</div>
<div class="d-inline-block expansion" v-text="item.key"></div>
</td>
Expand Down Expand Up @@ -1723,9 +1746,9 @@ <h2>{{ i18n.description }}</h2>
<div class="ml-1 d-flex mt-1 mt-sm-0">
<v-btn text small icon color="primary" v-if="!isEdit(`comment-${index}`)" @click="startEdit('comment-desc-input', comment.description, `comment-${index}`, 'description', modifyAssociation, ['comments', comment], true)"
@keypress.space.prevent="startEdit('comment-desc-input', comment.description, `comment-${index}`, 'description', modifyAssociation, ['comments', comment], true)">
<v-icon style="font-size: 14px !important;">fa-edit</v-icon>
<v-icon class="tiny-icon">fa-edit</v-icon>
</v-btn>
<v-btn text small color="red" @click="deleteAssociation('comments', comment)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('comments', comment)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</div>
Expand Down Expand Up @@ -1997,7 +2020,7 @@ <h3 class="text--primary">{{ i18n.attachmentAdd }}</h3>
{{ i18n.edited }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAssociation('attachments', props.item)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('attachments', props.item)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</td>
Expand Down Expand Up @@ -2285,7 +2308,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
{{ job.completeTime | formatDateTime }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAnalyzeJob(job)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAnalyzeJob(job)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
<v-spacer/>
</div>
Expand Down Expand Up @@ -2316,7 +2339,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
{{ i18n.edited }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAssociation('evidence', props.item)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('evidence', props.item)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</td>
Expand Down Expand Up @@ -2400,7 +2423,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
{{ props.item.createTime | formatDateTime }}
</div>
<div class="ml-1 d-flex">
<v-btn text small color="red" @click="deleteAssociation('events', props.item)" icon><v-icon style="font-size: 14px !important;">fa-trash</v-icon></v-btn>
<v-btn text small color="red" @click="deleteAssociation('events', props.item)" icon><v-icon class="tiny-icon">fa-circle-xmark</v-icon></v-btn>
</div>
</div>
</td>
Expand Down Expand Up @@ -2458,7 +2481,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
</td>
<td class="associated">
<v-icon v-if="props.item.operation == i18n.create">fa-plus</v-icon>
<v-icon v-if="props.item.operation == i18n.delete">fa-trash</v-icon>
<v-icon v-if="props.item.operation == i18n.delete">fa-circle-xmark</v-icon>
<v-icon v-if="props.item.operation == i18n.update">fa-pencil-alt</v-icon>
<span class="rounded ml-2">{{ props.item.operation }}</span>
</td>
Expand Down Expand Up @@ -3097,8 +3120,12 @@ <h4 v-if="metricsEnabled">{{ i18n.gridEps }} {{ gridEps | formatCount }}</h4>
</div>
<div class="text-no-wrap">
<span class="filter label">{{ i18n.osUptime }}:</span>
<span class="filter value" id="node_uptime" :class="areMetricsCurrent(item) ? '' : 'stale'">{{ item.osUptimeSeconds | formatDuration }}</span>
<span v-if="item.osNeedsRestart" id="node_restart-required-note" class="warning--text" :class="areMetricsCurrent(item) ? '' : 'stale'">{{ i18n.restartRequired }}</span>
<span class="filter value" id="node_uptime" :class="areMetricsCurrent(item) ? '' : 'stale'">
{{ item.osUptimeSeconds | formatDuration }}
<span v-if="item.osNeedsRestart" id="node_restart-required-note" class="warning--text" :class="areMetricsCurrent(item) ? '' : 'stale'">
{{ i18n.restartRequired }}
</span>
</span>
</div>
<div v-if="item.metricsEnabled" class="text-no-wrap">
<span class="filter label">{{ i18n.lastHighstate }}:</span>
Expand Down Expand Up @@ -3552,7 +3579,7 @@ <h2 v-text="i18n.settingsTitle"></h2>
<div :id="'settingsWebauthnExistingKey_' + key.id" class="ml-2">
{{ key.name }}
<v-btn name="webauthn_remove" icon :value="key.value" :id="'settingsWebauthnDelete_' + key.id" type="submit" color="primary" >
<v-icon>fa-trash</v-icon>
<v-icon>fa-circle-xmark</v-icon>
</v-btn>
</div>
</v-form>
Expand Down Expand Up @@ -3806,7 +3833,7 @@ <h4 v-if="!$root.loading">{{ i18n.settingsCustomized }} {{ settingsCustomized }}
<v-col>
<div class="mb-n1 text-right">
<v-btn :id="'setting-node-reset-' + item[0]" icon @click="remove(selected, item[0])" :title="i18n.settingRemoveHelp" :disabled="isPendingSave(selected, item[0])" >
<v-icon color="warning">fa-trash</v-icon>
<v-icon color="warning">fa-circle-xmark</v-icon>
</v-btn>
<v-btn :id="'setting-node-save-' + item[0]" icon @click="save(selected, item[0])" :disabled="!isPendingSave(selected, item[0])" :title="i18n.settingSaveHelp">
<v-icon color="success">fa-check</v-icon>
Expand Down Expand Up @@ -4032,7 +4059,7 @@ <h2 v-text="i18n.gridMembers"></h2>
</v-btn>
<v-btn id="gridmember-remove" @click.stop="confirmRemove()">
{{ i18n.delete }}
<v-icon right>fa-trash</v-icon>
<v-icon right>fa-circle-xmark</v-icon>
</v-btn>
</v-card-actions>
</v-card>
Expand Down
2 changes: 2 additions & 0 deletions html/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ const i18n = {
active: 'Active',
add: 'Add',
addAttachmentHelp: 'Add a new attachment to this case',
addColumnHeader: 'Toggle Table Column',
addColumnHeaderHelp: 'Add or remove this field as a table column',
addCommentHelp: 'Add a new comment to this case',
addInNewTab: 'Add in New Tab',
addObservableHelp: 'Add a new observable to this case',
Expand Down
145 changes: 135 additions & 10 deletions html/js/routes/hunt.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const huntComponent = {
queryGroupBys: [],
queryGroupByOptions: [],
querySortBys: [],
queryTableFields: [],
queryTableOptions: [],
eventFields: {},
dateRange: '',
relativeTimeEnabled: true,
Expand Down Expand Up @@ -137,6 +139,7 @@ const huntComponent = {
addToCaseDialogVisible: false,
mruCases: [],
selectedMruCase: null,
disableRouteLoad: false,
}},
created() {
this.$root.initializeCharts();
Expand Down Expand Up @@ -440,6 +443,8 @@ const huntComponent = {
return true;
},
async loadData() {
if (this.disableRouteLoad) return;

if (!this.parseUrlParameters()) return;

this.$root.startLoading();
Expand Down Expand Up @@ -682,6 +687,8 @@ const huntComponent = {
this.queryFilters = [];
this.queryGroupBys = [];
this.queryGroupByOptions = [];
this.queryTableFields = [];
this.queryTableOptions = [];
this.querySortBys = [];
var route = this;
if (this.query) {
Expand Down Expand Up @@ -751,6 +758,23 @@ const huntComponent = {
route.queryGroupBys.push(fields);
route.queryGroupByOptions.push(options);
}
if (segment.indexOf("table") == 0) {
var fields = [];
var options = [];
segment.split(" ").forEach(function(item, index) {
// Skip empty fields and segment options (they start with a hyphen)
if (item[0] == "-") {
options.push(item.substring(1));
} else if (index > 0 && item.trim().length > 0) {
if (item.split("\"").length % 2 == 1) {
// Will currently skip quoted items with spaces.
fields.push(item);
}
}
});
route.queryTableFields = fields;
route.queryTableOptions = options;
}
if (segment.indexOf("sortby") == 0) {
segment.split(" ").forEach(function(item, index) {
if (index > 0 && item.trim().length > 0) {
Expand Down Expand Up @@ -1054,15 +1078,17 @@ const huntComponent = {
},
constructHeaders(fields) {
var headers = [];
var i18n = this.i18n;
fields.forEach(function(item, index) {
var i18nKey = "field_" + item;
var header = {
text: i18n[i18nKey] ? i18n[i18nKey] : item,
value: item,
};
headers.push(header);
});
if (fields && fields.length > 0) {
var i18n = this.i18n;
fields.forEach(function(item, index) {
var i18nKey = "field_" + item;
var header = {
text: i18n[i18nKey] ? i18n[i18nKey] : item,
value: item,
};
headers.push(header);
});
}
return headers;
},
lookupSocId(data) {
Expand Down Expand Up @@ -1243,9 +1269,108 @@ const huntComponent = {
fields.push(key);
}
}
this.eventHeaders = this.constructHeaders(this.filterVisibleFields(eventModule, eventDataset, fields));
this.populateEventHeaders(this.filterVisibleFields(eventModule, eventDataset, fields));
this.eventData = records;
},
populateEventHeaders(defaultFields) {
var fields = defaultFields;
if (this.queryTableFields.length > 0) {
fields = this.queryTableFields;
}
this.eventHeaders = this.constructHeaders(fields);
},
repopulateEventHeaders() {
this.populateEventHeaders();

// This is a UI interaction so update the query and route to reflect the new table segment
var segments = this.query.split("|");
var newQuery = segments[0];
for (var i = 1; i < segments.length; i++) {
if (segments[i].trim().indexOf("table") == 0) {
continue;
}
newQuery = newQuery.trim() + " | " + segments[i].trim();
}
if (this.queryTableFields.length > 0) {
newQuery = newQuery + " | table " + this.queryTableFields.join(" ");
}

this.updateActiveQuery(newQuery);
},
updateActiveQuery(newQuery) {
var route = this.buildCurrentRoute();
route.query.q = newQuery;

this.disableRouteLoad = true;
this.$router.push(route);
this.query = newQuery;
const thisRoute = this;
setTimeout(function() { thisRoute.disableRouteLoad = false; }, 100);
},
toggleColumnHeader(field) {
if (!this.isColumnHeader(field)) {
this.addColumnHeader(field);
} else {
this.removeColumnHeader(field);
}
},
populateQueryTableFields() {
if (this.queryTableFields.length == 0) {
// Pre-populate with the default field headers already in eventHeaders (from populateEventTable)
for (const idx in this.eventHeaders) {
const field = this.eventHeaders[idx].value;
this.queryTableFields.push(field);
}
}
},
moveColumnHeader(field, left) {
this.populateQueryTableFields();
for (var idx = -1; idx < this.queryTableFields.length; idx++) {
const currentField = this.queryTableFields[idx];
if (field == currentField) {
break;
}
}

if (idx > -1) {
if (left) {
if (idx > 0) {
var tmpFields = this.queryTableFields.slice(0, idx - 1);
tmpFields.push(field);
tmpFields = tmpFields.concat(this.queryTableFields.slice(idx - 1, idx));
this.queryTableFields = tmpFields.concat(this.queryTableFields.slice(idx + 1));
}
} else {
if (idx < this.queryTableFields.length - 1) {
var tmpFields = this.queryTableFields.slice(0, idx);
tmpFields = tmpFields.concat(this.queryTableFields.slice(idx + 1, idx + 2));
tmpFields.push(field);
this.queryTableFields = tmpFields.concat(this.queryTableFields.slice(idx + 2));
}
}
}
this.repopulateEventHeaders(); // no defaults fields will be supplied since we know they aren't going to be used.
},
addColumnHeader(field) {
this.populateQueryTableFields();
this.queryTableFields.push(field);
this.repopulateEventHeaders(); // no defaults fields will be supplied since we know they aren't going to be used.
},
removeColumnHeader(field) {
this.populateQueryTableFields();
this.queryTableFields = this.queryTableFields.filter(item => item != field);

// do not revert back to the predefined headers if this was the last column that was just removed. Otherwise
// users would get frustrated if they're trying to remove all the columns to then add their own.
this.repopulateEventHeaders(); // no defaults fields provided
},
isColumnHeader(field) {
return this.eventHeaders.find((item) => {
if (item.value == field) {
return true;
}
}) != null;
},
displayTable(group, groupIdx) {
group.chart_type = "";
Vue.set(this.groupBys, groupIdx, group);
Expand Down
Loading
Loading