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

Manually Sync Detections From UI #400

Merged
merged 1 commit into from
Mar 29, 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
2 changes: 1 addition & 1 deletion config/clientparameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type ClientParameters struct {
CaseParams CaseParameters `json:"case"`
DashboardsParams HuntingParameters `json:"dashboards"`
JobParams HuntingParameters `json:"job"`
DetectionsParams HuntingParameters `json:"detections"`
DetectionsParams DetectionParameters `json:"detections"`
DetectionParams DetectionParameters `json:"detection"`
PlaybooksParams HuntingParameters `json:"playbooks"`
DocsUrl string `json:"docsUrl"`
Expand Down
19 changes: 19 additions & 0 deletions html/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,23 @@ td {
.override-help {
vertical-align: -webkit-baseline-middle;
margin-right: 8px;
}

.manual-sync {
margin: 6px;
display: flex;
}

.manual-sync > div:first-child {
flex-grow: 1;
margin-right: 8px;
}

.manual-sync-buttons {
display: flex;
flex-direction: column;
}

.manual-sync-buttons > button:first-child {
margin-bottom: 6px;
}
9 changes: 9 additions & 0 deletions html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,15 @@ <h2 v-text="i18n[category]"></h2>
<span :data-aid="'timezone_select_' + category">
<v-select :disabled="loading()" @change="saveTimezone" v-model="zone" :items="$root.timezones" :hint="i18n.timezoneHelp" persistent-hint prepend-icon="fa-user-clock"></v-select>
</span>
<div v-if="isCategory('detections')" class="manual-sync">
<div>
<v-select :items="getPresets('manualSync')" v-model="manualSyncTargetEngine" :hint="i18n.manualSyncHint" persistent-hint prepend-icon="fa-sync"></v-select>
</div>
<div class="manual-sync-buttons">
<v-btn @click="startManualSync(manualSyncTargetEngine, 'update')">{{i18n.manualSyncUpdate}}</v-btn>
<v-btn @click="startManualSync(manualSyncTargetEngine, 'full')">{{i18n.manualSyncFull}}</v-btn>
</div>
</div>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
Expand Down
15 changes: 14 additions & 1 deletion html/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -379,10 +379,23 @@ $(document).ready(function() {
if (u.host.toUpperCase() == window.location.host.toUpperCase()) {
url = u.hash;
}
const content = this.i18n.gridMemberImportSuccess.replace('<[url]>', url);
const content = this.i18n.gridMemberImportSuccess.replace('{url}', url);
this.showInfo(content);
}
});
this.subscribe('detection-sync', (report) => {
switch (report.status) {
case 'success':
this.showInfo(this.i18n.syncSuccess.replace('{engine}', report.engine));
break;
case 'partial':
this.showWarning(this.i18n.syncPartialSuccess.replace('{engine}', report.engine));
break;
case 'error':
this.showError(this.i18n.syncFailure.replace('{engine}', report.engine));
break;
}
});
}
} catch (error) {
if (!background) {
Expand Down
11 changes: 9 additions & 2 deletions html/js/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,7 @@ const i18n = {
gridMemberUploadConflict: 'A file with that name is currently being imported. Please wait for the current import to complete before trying again.',
gridMemberUploadFailure: 'Something went wrong while uploading the file. The file was not imported.',
gridMemberImportNoChanges: 'A recent import made no changes.',
// note: must replace <[url]> with a link to the results page
gridMemberImportSuccess: 'A recent import has completed and the <a id="view-results" href="<[url]>">results</a> will be available in Dashboards momentarily.',
gridMemberImportSuccess: 'A recent import has completed and the <a id="view-results" href="{url}">results</a> will be available in Dashboards momentarily.',
gridMemberRestartConfirmTitle: 'Reboot Node',
gridMemberRestartConfirmHelp: 'Rebooting a node may be required for various reasons, such as if the node has installed new kernel updates.<p class="my-3"/>The grid may show a fault for several minutes while the node reboots and starts the Security Onion services.<p class="my-3"/>⚠️ Rebooting the manager node will temporarily prevent access to this web application and may show an error similar to <b>502 Bad Gateway</b>. Wait a few minutes and then refresh the browser window to regain access.',
gridMemberRestartSuccess: 'Successfully issued a reboot request to the node.',
Expand Down Expand Up @@ -524,6 +523,9 @@ const i18n = {
loginTitle: 'Login to Security Onion',
logout: 'Logout',
logoutFailure: 'Unable to initiate logout. Ensure server is accessible.',
manualSyncFull: 'Full Update',
manualSyncUpdate: 'Differential Update',
manualSyncHint: 'Select an engine to synchronize',
markdownFormattingSupported: 'Markdown formatting supported',
maximize: 'Maximize View (ESC to cancel)',
maxUploadSize: 'Maximum upload size',
Expand Down Expand Up @@ -779,6 +781,8 @@ const i18n = {
standardMetrics: 'Basic Metrics',
startEndNumericErr: 'Start and End values must be numeric.',
startEndOrderErr: 'Start value must come before End value.',
startSyncFull: 'Started a full sync of {engine} detections.',
startSyncUpdate: 'Started an update sync of {engine} detections.',
status: 'Status',
stenoLoss: 'Stenographer Loss',
stenoLossAbbr: 'Steno Loss',
Expand All @@ -787,6 +791,9 @@ const i18n = {
suricataLoss: 'Suricata Loss',
suricataLossAbbr: 'Suri Loss',
swapUsage: 'Swap Usage',
syncSuccess: 'Synchronized {engine} community rules successfully.',
syncPartialSuccess: 'Synchronized {engine} community rules with some errors. Check SOC logs for details.',
syncFailure: 'Something went wrong attempting to synchronize {engine} community rules. Check SOC logs for details.',
tags: 'Tags',
thresholdType: 'Threshold Type',
throttledLogin: 'Excessive login requests detected. Login requests can resume momentarily.',
Expand Down
109 changes: 38 additions & 71 deletions html/js/routes/hunt.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ const huntComponent = {
{ text: this.$root.i18n.disable, value: 'disable' },
],
quickActionDetId: null,
presets: {},
manualSyncTargetEngine: null,
}},
created() {
this.$root.initializeCharts();
Expand Down Expand Up @@ -245,6 +247,7 @@ const huntComponent = {
this.chartLabelMaxLength = params["chartLabelMaxLength"]
this.chartLabelOtherLimit = params["chartLabelOtherLimit"]
this.chartLabelFieldSeparator = params["chartLabelFieldSeparator"]
this.presets = params["presets"];
if (this.queries != null && this.queries.length > 0) {
this.query = this.queries[0].query;
}
Expand Down Expand Up @@ -479,78 +482,20 @@ const huntComponent = {
// This must occur before the following await, so that Vue flushes the old groupby DOM renders
this.groupBys.splice(0);

let response;
if (this.category === 'playbooks') {
response = {
data: {
"metrics": {
"timeline": null,
},
"elapsedMs": 668,
"errors": [],
"criteria": {
"query": "(_id:*) AND _index:\"*:so-case\" AND so_kind:detection",
"dateRange": "",
"metricLimit": 0,
"eventLimit": 50,
"BeginTime": "2021-08-23T15:41:39-06:00",
"EndTime": "2023-08-23T15:41:39-06:00",
"CreateTime": "2023-08-23T15:41:39.446264196-06:00",
"ParsedQuery": {
"Segments": [
{}
]
},
"SortFields": null
},
"events": [
{
"source": "manager:so-case",
"Time": "2023-08-23T14:48:17.140438075-06:00",
"timestamp": "2023-08-23T14:48:17.140Z",
"id": "RDmUHooB-8rNCo4d3nIc",
"type": "",
"score": 3.287682,
"payload": {
"@timestamp": "2023-08-23T14:48:17.140438075-06:00",
"so_playbook.onionId": "75332a3c-b029-46a0-9392-509ff90737a8",
"so_playbook.publicId": "4020131e-223a-421e-8ebe-8a211a5ac4d6",
"so_playbook.title": "Find the baddies",
"so_playbook.severity": "high",
"so_playbook.description": "A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines. A long description that spans multiple lines.",
"so_playbook.mechanism": "suricata",
"so_playbook.tags": ["one", "two", "three"],
"so_playbook.relatedPlaybooks": [],
"so_playbook.contributors": ["Jim Bob"],
"so_playbook.userEditable": true,
"so_playbook.createTime": "2023-08-22T12:49:47.302819008-06:00",
"so_playbook.kind": "playbook",
"so_playbook.userId": "83656890-2acd-4c0b-8ab9-7c73e71ddaf3",
"so_kind": "playbook"
}
}
],
createTime: moment().subtract(2, 'seconds').toISOString(),
completeTime: moment().toISOString(),
}
};
response.data.totalEvents = response.data.events.length;
} else {
let range = this.dateRange;
if (this.isCategory('detections')) {
range = moment(0).format(this.i18n.timePickerFormat) + " - " + moment().format(this.i18n.timePickerFormat);
let range = this.dateRange;
if (this.isCategory('detections')) {
range = moment(0).format(this.i18n.timePickerFormat) + " - " + moment().format(this.i18n.timePickerFormat);
}
let response = await this.$root.papi.get('events/', {
params: {
query: await this.getQuery(),
range: range,
format: this.i18n.timePickerSample,
zone: this.zone,
metricLimit: this.groupByLimit,
eventLimit: this.eventLimit
}
response = await this.$root.papi.get('events/', {
params: {
query: await this.getQuery(),
range: range,
format: this.i18n.timePickerSample,
zone: this.zone,
metricLimit: this.groupByLimit,
eventLimit: this.eventLimit
}
});
}
});

this.eventPage = 1;
this.groupByPage = 1;
Expand Down Expand Up @@ -578,6 +523,13 @@ const huntComponent = {
}
this.$root.stopLoading();
},
getPresets(kind) {
if (this.presets && this.presets[kind]) {
return this.presets[kind].labels;
}

return [];
},
async filterQuery(field, value, filterMode, notify = true, scalar = false) {
try {
const valueType = typeof value;
Expand Down Expand Up @@ -2307,6 +2259,21 @@ const huntComponent = {
this.$root.showInfo(msg);
}
},
startManualSync(engine, type) {
try {
this.$root.papi.post(`detection/sync/${engine}/${type}`);

let msg = this.i18n.startSyncFull;
if (type !== 'full') {
msg = this.i18n.startSyncUpdate;
}

msg = msg.replace("{engine}", engine);
this.$root.showTip(msg);
} catch (e) {
this.$root.showError(e);
}
},
}
};

Expand Down
7 changes: 6 additions & 1 deletion server/detectionengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ type DetectionEngine interface {
SyncLocalDetections(ctx context.Context, detections []*model.Detection) (errMap map[string]string, err error)
ConvertRule(ctx context.Context, detect *model.Detection) (string, error)
ExtractDetails(detect *model.Detection) error
InterruptSleep()
InterruptSleep(forceFull bool)
}

type SyncStatus struct {
Engine model.EngineName `json:"engine"`
Status string `json:"status"`
}
24 changes: 24 additions & 0 deletions server/detectionhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func RegisterDetectionRoutes(srv *Server, r chi.Router, prefix string) {
r.Delete("/{id}", h.deleteDetection)

r.Post("/bulk/{newStatus}", h.bulkUpdateDetection)
r.Post("/sync/{engine}/{type}", h.syncEngineDetections)
})
}

Expand Down Expand Up @@ -588,3 +589,26 @@ func (h *DetectionHandler) convertContent(w http.ResponseWriter, r *http.Request
"query": eaQuery,
})
}

func (h *DetectionHandler) syncEngineDetections(w http.ResponseWriter, r *http.Request) {
engine := strings.ToLower(chi.URLParam(r, "engine"))
typ := strings.ToLower(chi.URLParam(r, "type"))

fullUpgrade := typ == "full"

if engine == "all" {
for _, engine := range h.server.DetectionEngines {
engine.InterruptSleep(fullUpgrade)
}
} else {
engine, ok := h.server.DetectionEngines[model.EngineName(engine)]
if !ok {
web.Respond(w, r, http.StatusBadRequest, errors.New("unknown engine"))
return
}

engine.InterruptSleep(fullUpgrade)
}

web.Respond(w, r, http.StatusOK, nil)
}
Loading
Loading