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

Detection History #335

Merged
merged 1 commit into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
172 changes: 168 additions & 4 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1376,9 +1376,173 @@ <h2 id="detection-title" @click="startEdit('detection-title', 'title')" v-if="!i
</div>
</v-tab-item>
<v-tab-item value="history">
<div class="col" style="background-color: rgb(53, 53, 53); border-radius: 4px;">
Coming Soon!
</div>
<v-row>
<v-col>
<v-toolbar class="elevation-0" fixed>
<v-btn :title="i18n.refreshDetectionHistoryHelp" color="secondary" @click.stop="loadHistory()" id="refresh-history-button">
<v-icon>fa-sync</v-icon>
</v-btn>
</v-toolbar>
</v-col>
</v-row>
<!-- <div class="col" style="background-color: rgb(53, 53, 53); border-radius: 4px;"> -->
<v-text-field v-model="historyTableOpts.search" clearable prepend-icon="fa-filter" :label="i18n.filterResults" single-line hide-details></v-text-field>
<v-data-table ref="historyTable" :sort-by.sync="historyTableOpts.sortBy" :sort-desc.sync="historyTableOpts.sortDesc" :items-per-page.sync="historyTableOpts.itemsPerPage"
:search="historyTableOpts.search" :footer-props="historyTableOpts.footerProps" must-sort :headers="historyTableOpts.headers"
:items="history" item-key="id" :loading="historyTableOpts.loading" :expanded="historyTableOpts.expanded" class="history-table">
<template v-slot:item="props">
<tr>
<td class="associated actions">
<v-btn :id="`history-${props.index}-expand`" icon small @click="expandRow(props.item)">
<v-icon v-if="isExpanded(props.item)" :alt="i18n.expand" :title="i18n.expandHelp">fa-angle-down</v-icon>
<v-icon v-if="!isExpanded(props.item)" :alt="i18n.expand" :title="i18n.expandHelp">fa-angle-right</v-icon>
</v-btn>
</td>
<td class="associated">
<v-avatar color="primary" class="mr-2 white--text" size="28">
<div class="font-weight-bold" :title="props.item.owner">
{{ $root.getAvatar(props.item.owner) }}
</div>
</v-avatar>
{{ props.item.owner }}
</td>
<td class="associated">{{ props.item.updateTime | formatDateTime }}</td>
<td class="associated">
<v-icon>fa-magnifying-glass</v-icon>
<span class="rounded ml-2">{{ props.item.kind }}</span>
</td>
<td class="associated">
<v-icon v-if="props.item.operation.toLowerCase() == i18n.create.toLowerCase()">fa-plus</v-icon>
<v-icon v-if="props.item.operation.toLowerCase() == i18n.delete.toLowerCase()">fa-circle-xmark</v-icon>
<v-icon v-if="props.item.operation.toLowerCase() == i18n.update.toLowerCase()">fa-pencil-alt</v-icon>
<span class="rounded ml-2">{{ props.item.operation }}</span>
</td>
</tr>
</template>
<template v-slot:expanded-item="props">
<tr>
<td colspan="5" class="case">
<div class="mt-2">
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.id }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.id }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.kind }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.kind }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.operation }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.operation }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.dateCreated }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.createTime | formatDateTime }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.dateModified }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.updateTime | formatDateTime }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.title }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.title }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.description }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.description }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.author }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.author }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.enabled }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.isEnabled ? 'True' : 'False' }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.community }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.isCommunity ? 'True' : 'False' }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.reporting }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.isReporting ? 'True' : 'False' }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.severity }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.severity }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.engine }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.engine }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.content }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.content }}</span></v-col>
</v-row>
</div>
<div>
<v-row no-gutters class="py-1 case tabular-row">
<v-col cols="4"><span class="case tabular-label">{{ i18n.note }}:</span></v-col>
<v-col cols="8"><span class="case">{{ props.item.note }}</span></v-col>
</v-row>
</div>

<div class="d-flex flex-column flex-sm-row align-end align-sm-center align-self-end text-body-2">
<v-spacer/>
<v-avatar color="primary" size="24" class="mr-2 d-none d-sm-inline-flex white--text">
<div class="case avatar-font" :title="props.item.owner">
{{ $root.getAvatar(props.item.owner) }}
</div>
</v-avatar>
<div class="mr-1">
{{ props.item.owner }}
</div>
<div class="mr-1 d-none d-sm-block">
&bull;
</div>
<div>
{{ props.item.updateTime | formatDateTime }}
</div>
</div>
</td>
</tr>
</template>
<template v-slot:no-results>
<v-alert :value="true" color="info" icon="fa-info">
{{ i18n.noSearchResults }}
</v-alert>
</template>
</v-data-table>
<div class="text-xs-center pt-2 d-none">
<v-btn v-text="i18n.loadMore" @click="loadHistory()"></v-btn>
</div>
<!-- </div> -->
</v-tab-item>
</v-tabs>
</div>
Expand Down Expand Up @@ -3091,7 +3255,7 @@ <h3 class="text--primary">{{ i18n.evidenceAdd }}</h3>
<v-row>
<v-col>
<v-toolbar class="elevation-0" fixed>
<v-btn :title="i18n.refreshHistoryHelp" color="secondary" @click.stop="reloadAssociation('history', true)" id="refresh-history-button">
<v-btn :title="i18n.refreshCaseHistoryHelp" color="secondary" @click.stop="reloadAssociation('history', true)" id="refresh-history-button">
<v-icon>fa-sync</v-icon>
</v-btn>
</v-toolbar>
Expand Down
7 changes: 6 additions & 1 deletion html/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ const i18n = {
commentHours: 'Work Hours',
commentHoursHelp: 'Hours spent working on this update (leave blank for 0 hours)',
commentRequired: 'Comments cannot be empty.',
community: 'Community',
completed: 'Completed',
config: 'Configuration',
configTitle: 'Grid Configuration',
Expand Down Expand Up @@ -207,6 +208,7 @@ const i18n = {
configQLZeekThreads: 'Change number of Zeek workers (threads)',
container: 'Container',
containerStatus: 'Container Status',
content: 'Content',
continue: 'Would you like to continue?',
contributors: 'Contributors',
copyEventToClipboardAsJson: 'Copy full event as JSON',
Expand Down Expand Up @@ -293,6 +295,7 @@ const i18n = {
emailRequired: 'An email address must be specified.',
enable: 'Enable',
enabled: 'Enabled',
enabledLabel: 'Enabled:',
endTime: 'Filter End',
endTimeHelp: 'Filter end time in RFC 3339 format (Ex: 2020-10-16 15:30:00.230-04:00). Unused for imported PCAPs.',
engine: 'Engine',
Expand Down Expand Up @@ -596,10 +599,11 @@ const i18n = {
redisQueueSize: 'Redis Queue Size',
refresh: 'Refresh',
refreshAttachmentsHelp: 'Refresh to view all recently added attachments for this case.',
refreshCaseHistoryHelp: 'Refresh to view the latest history for this case.',
refreshDetectionHistoryHelp: 'Refresh to view the latest history for this detection.',
refreshCommentsHelp: 'Refresh to view all recently added comments for this case.',
refreshObservablesHelp: 'Refresh to view all recently added observables for this case.',
refreshEventsHelp: 'Refresh to view all recently escalated events for this case.',
refreshHistoryHelp: 'Refresh to view the latest history for this case.',
regex: 'Regex',
reject: 'Reject',
rejected: 'Rejected',
Expand Down Expand Up @@ -755,6 +759,7 @@ const i18n = {
timestampFormat: 'YYYY-MM-DD HH:mm:ss.SSS Z',
timezone: 'Time Zone:',
timezoneHelp: 'Time Zone',
title: 'Title',
toggleLegend: 'Toggle Legend',
toolCyberchef: 'CyberChef',
toolCyberchefHelp: 'Data decoding and transformation tools',
Expand Down
55 changes: 53 additions & 2 deletions html/js/routes/detection.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {
{ value: 'limit', text: 'Limit' },
{ value: 'both', text: 'Both' }
],
historyTableOpts: {
sortBy: 'updateTime',
sortDesc: false,
search: '',
headers: [
{ text: this.$root.i18n.actions, width: '10.0em' },
{ text: this.$root.i18n.username, value: 'owner' },
{ text: this.$root.i18n.time, value: 'updateTime' },
{ text: this.$root.i18n.kind, value: 'kind' },
{ text: this.$root.i18n.operation, value: 'operation' },
],
itemsPerPage: 10,
footerProps: { 'items-per-page-options': [10,50,250,1000] },
count: 500,
expanded: [],
loading: false,
},
history: [],
}},
created() {
this.onDetectionChange = debounce(this.onDetectionChange, 300);
Expand Down Expand Up @@ -126,6 +144,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {
const response = await this.$root.papi.get('detection/' + encodeURIComponent(this.$route.params.id));
this.detect = response.data;
this.tagOverrides();
this.loadAssociations();
} catch (error) {
if (error.response != undefined && error.response.status == 404) {
this.$root.showError(this.i18n.notFound);
Expand All @@ -136,6 +155,17 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {

this.$root.stopLoading();
},
loadAssociations() {
this.loadHistory();
},
async loadHistory() {
const route = this;
const id = route.$route.params.id;
const response = await this.$root.papi.get(`detection/${id}/history`);
if (response && response.data) {
this.history = response.data;
}
},
getDefaultPreset(preset) {
if (this.presets) {
const presets = this.presets[preset];
Expand Down Expand Up @@ -632,6 +662,8 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {
}
}
}
} else if (this.detect.engine === 'strelka') {
return false;
}

return true;
Expand All @@ -641,10 +673,29 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {
for (let i = 0; i < this.detect.overrides.length; i++) {
this.detect.overrides[i].index = i;
}
} else {
this.detect.overrides = [];
}
},
print(x) {
console.log(x);
isExpanded(row) {
const expanded = this.historyTableOpts.expanded;
for (var i = 0; i < expanded.length; i++) {
if (expanded[i].id == row.id) {
return true;
}
}
return false;
},
async expandRow(row) {
const expanded = this.historyTableOpts.expanded;
for (var i = 0; i < expanded.length; i++) {
if (expanded[i].id == row.id) {
expanded.splice(i, 1);
return;
}
}

expanded.push(row);
}
}
}});
19 changes: 19 additions & 0 deletions server/detectionhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ func RegisterDetectionRoutes(srv *Server, r chi.Router, prefix string) {
r.Post("/", h.postDetection)
r.Post("/{id}/duplicate", h.duplicateDetection)

r.Get("/{id}/history", h.getDetectionHistory)

r.Put("/", h.putDetection)

r.Delete("/{id}", h.deleteDetection)
Expand Down Expand Up @@ -115,6 +117,23 @@ func (h *DetectionHandler) postDetection(w http.ResponseWriter, r *http.Request)
web.Respond(w, r, http.StatusOK, detect)
}

func (h *DetectionHandler) getDetectionHistory(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

id := chi.URLParam(r, "id")
if id == "" {
id = r.URL.Query().Get("id")
}

obj, err := h.server.Detectionstore.GetDetectionHistory(ctx, id)
if err != nil {
web.Respond(w, r, http.StatusNotFound, err)
return
}

web.Respond(w, r, http.StatusOK, obj)
}

func (h *DetectionHandler) duplicateDetection(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

Expand Down
1 change: 1 addition & 0 deletions server/detectionstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Detectionstore interface {
DeleteDetection(ctx context.Context, detectID string) (*model.Detection, error)
GetAllCommunitySIDs(ctx context.Context, engine *model.EngineName) (map[string]*model.Detection, error) // map[detection.PublicId]detection
Query(ctx context.Context, query string, max int) ([]interface{}, error)
GetDetectionHistory(ctx context.Context, detectID string) ([]interface{}, error)
}

//go:generate mockgen -destination mock/mock_detectionstore.go -package mock . Detectionstore
15 changes: 15 additions & 0 deletions server/mock/mock_detectionstore.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions server/modules/elastic/elasticdetectionstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,13 @@ func (store *ElasticDetectionstore) GetAllCommunitySIDs(ctx context.Context, eng
return sids, nil
}

func (store *ElasticDetectionstore) GetDetectionHistory(ctx context.Context, detectID string) ([]interface{}, error) {
query := fmt.Sprintf(`_index:"%s" AND %s%s:"%s" | sortby @timestamp^`, store.auditIndex, store.schemaPrefix, AUDIT_DOC_ID, detectID)
history, err := store.Query(ctx, query, store.maxAssociations)

return history, err
}

func (store *ElasticDetectionstore) audit(ctx context.Context, document map[string]interface{}, id string) error {
var err error

Expand Down
Loading