Skip to content

Commit

Permalink
Merge pull request #400 from Security-Onion-Solutions/cogburn/manual-…
Browse files Browse the repository at this point in the history
…sync

Manually Sync Detections From UI
  • Loading branch information
coreyogburn authored Mar 29, 2024
2 parents 422cd57 + 4324909 commit 20981eb
Show file tree
Hide file tree
Showing 11 changed files with 406 additions and 129 deletions.
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

0 comments on commit 20981eb

Please sign in to comment.