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 Templates #530

Merged
merged 1 commit into from
May 31, 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
54 changes: 31 additions & 23 deletions config/clientparameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,27 @@ const DEFAULT_CHART_LABEL_OTHER_LIMIT = 10
const DEFAULT_CHART_LABEL_FIELD_SEPARATOR = ", "

type ClientParameters struct {
HuntingParams HuntingParameters `json:"hunt"`
AlertingParams HuntingParameters `json:"alerts"`
CasesParams HuntingParameters `json:"cases"`
CaseParams CaseParameters `json:"case"`
DashboardsParams HuntingParameters `json:"dashboards"`
JobParams HuntingParameters `json:"job"`
DetectionsParams DetectionParameters `json:"detections"`
DetectionParams DetectionParameters `json:"detection"`
DocsUrl string `json:"docsUrl"`
CheatsheetUrl string `json:"cheatsheetUrl"`
ReleaseNotesUrl string `json:"releaseNotesUrl"`
GridParams GridParameters `json:"grid"`
WebSocketTimeoutMs int `json:"webSocketTimeoutMs"`
TipTimeoutMs int `json:"tipTimeoutMs"`
ApiTimeoutMs int `json:"apiTimeoutMs"`
CacheExpirationMs int `json:"cacheExpirationMs"`
InactiveTools []string `json:"inactiveTools"`
Tools []ClientTool `json:"tools"`
CasesEnabled bool `json:"casesEnabled"`
EnableReverseLookup bool `json:"enableReverseLookup"`
DetectionsEnabled bool `json:"detectionsEnabled"`
HuntingParams HuntingParameters `json:"hunt"`
AlertingParams HuntingParameters `json:"alerts"`
CasesParams HuntingParameters `json:"cases"`
CaseParams CaseParameters `json:"case"`
DashboardsParams HuntingParameters `json:"dashboards"`
JobParams HuntingParameters `json:"job"`
DetectionsParams DetectionsParameters `json:"detections"`
DetectionParams DetectionParameters `json:"detection"`
DocsUrl string `json:"docsUrl"`
CheatsheetUrl string `json:"cheatsheetUrl"`
ReleaseNotesUrl string `json:"releaseNotesUrl"`
GridParams GridParameters `json:"grid"`
WebSocketTimeoutMs int `json:"webSocketTimeoutMs"`
TipTimeoutMs int `json:"tipTimeoutMs"`
ApiTimeoutMs int `json:"apiTimeoutMs"`
CacheExpirationMs int `json:"cacheExpirationMs"`
InactiveTools []string `json:"inactiveTools"`
Tools []ClientTool `json:"tools"`
CasesEnabled bool `json:"casesEnabled"`
EnableReverseLookup bool `json:"enableReverseLookup"`
DetectionsEnabled bool `json:"detectionsEnabled"`
}

func (config *ClientParameters) Verify() error {
Expand Down Expand Up @@ -190,15 +190,23 @@ type GridParameters struct {
StaleMetricsMs uint64 `json:"staleMetricsMs,omitempty"`
}

type DetectionParameters struct {
type DetectionsParameters struct {
HuntingParameters
DetectionParameters
}

type DetectionParameters struct {
Presets map[string]PresetParameters `json:"presets"`
SeverityTranslations map[string]string `json:"severityTranslations"`
TemplateDetections map[string]string `json:"templateDetections"`
}

func (params *DetectionParameters) Verify() error {
func (params *DetectionsParameters) Verify() error {
err := params.HuntingParameters.Verify()

return err
}

func (params *DetectionParameters) Verify() error {
return nil
}
2 changes: 1 addition & 1 deletion html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ <h2 id="detection-title">{{ detect.title }}</h2>
<div class="header">{{ i18n.add }} {{ i18n.detection }}</div>
<!-- Language -->
<span data-aid="detection_new_language_select">
<v-select id="detection-language-create" v-model="detect.language" :items="getPresets('language')" persistent-hint :hint="i18n.language" :rules="[rules.required]" v-on:change="onDetectionChange"/>
<v-select id="detection-language-create" v-model="detect.language" :items="getPresets('language')" persistent-hint :hint="i18n.language" :rules="[rules.required]" v-on:change="onNewDetectionLanguageChange"/>
</span>

<!-- License -->
Expand Down
28 changes: 28 additions & 0 deletions html/js/routes/detection.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {
convertedRule: '',
confirmDeleteDialog: false,
showDirtySourceDialog: false,
ruleTemplates: {},
languageToEngine: {
'suricata': 'suricata',
'sigma': 'elastalert',
'yara': 'strelka',
},
}},
created() {
this.onDetectionChange = debounce(this.onDetectionChange, 300);
Expand All @@ -141,6 +147,7 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {
this.presets = params['presets'];
this.renderAbbreviatedCount = params["renderAbbreviatedCount"];
this.severityTranslations = params['severityTranslations'];
this.ruleTemplates = params['templateDetections'];

if (this.$route.params.id === 'create') {
this.detect = this.newDetection();
Expand Down Expand Up @@ -813,6 +820,27 @@ routes.push({ path: '/detection/:id', name: 'detection', component: {
}
}
},
async onNewDetectionLanguageChange() {
const lang = (this.detect.language || '').toLowerCase();
const engine = this.languageToEngine[lang];

if (engine) {
let publicId = '';

if (engine !== 'strelka') {
try {
const response = await this.$root.papi.get(`detection/${engine}/genpublicid`);
publicId = response.data.publicId;
} catch (error) {
this.$root.showError(error);
}
}

this.detect.content = this.ruleTemplates[engine].replaceAll('[publicId]', publicId);
}

this.onDetectionChange();
},
onDetectionChange() {
if (this.detect.engine) {
this.extractPublicID();
Expand Down
38 changes: 33 additions & 5 deletions html/js/routes/detection.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ test('deleteDetectionCancel', () => {
comp.cancelDeleteDetection();
expect(comp.confirmDeleteDialog).toBe(false);
comp.deleteDetection();
})
});

test('deleteDetectionFailure', async () => {
resetPapi().mockPapi("delete", null, new Error("something bad"));
Expand Down Expand Up @@ -389,17 +389,45 @@ test('revertEnabled', () => {
comp.revertEnabled();
expect(comp.detect.isEnabled).toBe(false);
expect(comp.origDetect.isEnabled).toBe(false);
})
});

test('isFieldValid', () => {
comp.$refs = {}
expect(comp.isFieldValid('foo')).toBe(true)

comp.$refs = {bar: { valid: false}}
comp.$refs = { bar: { valid: false } }
expect(comp.isFieldValid('foo')).toBe(true)
expect(comp.isFieldValid('bar')).toBe(false)

comp.$refs = {bar: { valid: true}}
comp.$refs = { bar: { valid: true } }
expect(comp.isFieldValid('bar')).toBe(true)
});

})
test('onNewDetectionLanguageChange', async () => {
comp.ruleTemplates = {
"suricata": 'a [publicId]',
"strelka": 'b [publicId]',
"elastalert": 'c [publicId]',
}
// no language means no engine means no request means no change
comp.detect = { language: '', content: 'x' };
await comp.onNewDetectionLanguageChange();
expect(comp.detect.content).toBe('x');

// yara, no publicId, results in template without publicId
comp.detect = { language:'yara', content: 'x' };
await comp.onNewDetectionLanguageChange();
expect(comp.detect.content).toBe('b ');

// suricata, sid, results in template with publicId
resetPapi().mockPapi("get", { data: { publicId: 'X' } }, null);
comp.detect = { language:'suricata', content: 'x' };
await comp.onNewDetectionLanguageChange();
expect(comp.detect.content).toBe('a X');

// sigma, uuid, results in template with publicId
resetPapi().mockPapi("get", { data: { publicId: 'X' } }, null);
comp.detect = { language:'sigma', content: 'x' };
await comp.onNewDetectionLanguageChange();
expect(comp.detect.content).toBe('c X');
});
1 change: 1 addition & 0 deletions server/detectionengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type DetectionEngine interface {
InterruptSync(forceFull bool, notify bool)
DuplicateDetection(ctx context.Context, detection *model.Detection) (*model.Detection, error)
GetState() *model.EngineState
GenerateUnusedPublicId(ctx context.Context) (string, error)
}

type SyncStatus struct {
Expand Down
28 changes: 28 additions & 0 deletions server/detectionhandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ func RegisterDetectionRoutes(srv *Server, r chi.Router, prefix string) {

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

r.Get("/{engine}/genpublicid", h.genPublicId)
})
}

Expand Down Expand Up @@ -683,6 +685,32 @@ func (h *DetectionHandler) syncEngineDetections(w http.ResponseWriter, r *http.R
web.Respond(w, r, http.StatusOK, nil)
}

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

engine := chi.URLParam(r, "engine")

eng, ok := h.server.DetectionEngines[model.EngineName(engine)]
if !ok {
web.Respond(w, r, http.StatusBadRequest, errors.New("unsupported engine"))
return
}

id, err := eng.GenerateUnusedPublicId(ctx)
if err != nil {
if err.Error() == "not implemented" {
web.Respond(w, r, http.StatusNotImplemented, nil)
} else {
web.Respond(w, r, http.StatusInternalServerError, err)
}
return
}

web.Respond(w, r, http.StatusOK, map[string]string{
"publicId": id,
})
}

func (h *DetectionHandler) PrepareForSave(ctx context.Context, detect *model.Detection, e DetectionEngine) error {
err := e.ExtractDetails(detect)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions server/modules/elastalert/elastalert.go
Original file line number Diff line number Diff line change
Expand Up @@ -1333,7 +1333,7 @@ func (e *ElastAlertEngine) sigmaToElastAlert(ctx context.Context, det *model.Det
return query, nil
}

func (e *ElastAlertEngine) generateUnusedPublicId(ctx context.Context) (string, error) {
func (e *ElastAlertEngine) GenerateUnusedPublicId(ctx context.Context) (string, error) {
id := uuid.New().String()

i := 0
Expand All @@ -1359,7 +1359,7 @@ func (e *ElastAlertEngine) generateUnusedPublicId(ctx context.Context) (string,
}

func (e *ElastAlertEngine) DuplicateDetection(ctx context.Context, detection *model.Detection) (*model.Detection, error) {
id, err := e.generateUnusedPublicId(ctx)
id, err := e.GenerateUnusedPublicId(ctx)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion server/modules/elastalert/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func TestGenerateUnusedPublicId(t *testing.T) {
isRunning: true,
}

id, err := eng.generateUnusedPublicId(ctx)
id, err := eng.GenerateUnusedPublicId(ctx)

assert.Empty(t, id)
assert.Error(t, err)
Expand Down
6 changes: 6 additions & 0 deletions server/modules/strelka/strelka.go
Original file line number Diff line number Diff line change
Expand Up @@ -1003,6 +1003,12 @@ func (e *StrelkaEngine) DuplicateDetection(ctx context.Context, detection *model
return det, nil
}

func (e *StrelkaEngine) GenerateUnusedPublicId(ctx context.Context) (string, error) {
// PublicIDs for Strelka are the rule name which should correlate with what the rule does.
// Cannot generate arbitrary but still useful public IDs
return "", fmt.Errorf("not implemented")
}

func (e *StrelkaEngine) IntegrityCheck(canInterrupt bool) error {
// escape
if canInterrupt && !e.IntegrityCheckerData.IsRunning {
Expand Down
4 changes: 2 additions & 2 deletions server/modules/suricata/suricata.go
Original file line number Diff line number Diff line change
Expand Up @@ -1506,7 +1506,7 @@ func lookupLicense(ruleset string) string {
return license
}

func (e *SuricataEngine) generateUnusedPublicId(ctx context.Context) (string, error) {
func (e *SuricataEngine) GenerateUnusedPublicId(ctx context.Context) (string, error) {
id := strconv.Itoa(rand.IntN(1000000) + 1000000) // [1000000, 2000000)

i := 0
Expand All @@ -1532,7 +1532,7 @@ func (e *SuricataEngine) generateUnusedPublicId(ctx context.Context) (string, er
}

func (e *SuricataEngine) DuplicateDetection(ctx context.Context, detection *model.Detection) (*model.Detection, error) {
id, err := e.generateUnusedPublicId(ctx)
id, err := e.GenerateUnusedPublicId(ctx)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion server/modules/suricata/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func TestGenerateUnusedPublicId(t *testing.T) {
isRunning: true,
}

id, err := eng.generateUnusedPublicId(ctx)
id, err := eng.GenerateUnusedPublicId(ctx)

assert.Empty(t, id)
assert.Error(t, err)
Expand Down
Loading