-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Security Solution] Siem signals -> alerts as data field and index aliases #106049
Changes from 11 commits
010f093
c4edc7d
f943313
5f11d31
979ad9a
3148569
3e08e73
58f7464
1788952
9377e0c
df9b010
bad2321
b837f27
94f9dee
28722b1
185f4ba
bf89a05
30ac118
3c6f7b7
3c26be4
f1f5ddc
58d0e01
e8f464c
3054481
d7bc0da
0910131
3827188
0cd9b83
5973dde
ded440e
99d83ee
8e7e00e
c033517
fc475fd
5828090
4275da7
0214b61
498f9c4
6bcb6ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,7 +96,8 @@ export class RuleDataClient implements IRuleDataClient { | |
if (response.body.errors) { | ||
if ( | ||
response.body.items.length > 0 && | ||
response.body.items?.[0]?.index?.error?.type === 'index_not_found_exception' | ||
(response.body.items?.[0]?.index?.error?.type === 'index_not_found_exception' || | ||
response.body.items?.[0]?.index?.error?.type === 'illegal_argument_exception') | ||
) { | ||
return this.createWriteTargetIfNeeded({ namespace }).then(() => { | ||
return clusterClient.bulk(requestWithDefaultParameters); | ||
|
@@ -116,13 +117,14 @@ export class RuleDataClient implements IRuleDataClient { | |
|
||
const clusterClient = await this.getClusterClient(); | ||
|
||
const { body: aliasExists } = await clusterClient.indices.existsAlias({ | ||
name: alias, | ||
const { body: indicesExist } = await clusterClient.indices.exists({ | ||
index: `${alias}-*`, | ||
allow_no_indices: false, | ||
Comment on lines
+129
to
+131
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confused a bit, why do we need to check this? If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In some scenarios we could get an |
||
}); | ||
|
||
const concreteIndexName = `${alias}-000001`; | ||
|
||
if (!aliasExists) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed this check so that we can add the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This check has now been replaced with a more specific check - querying to find concrete indices rather than the alias. |
||
if (!indicesExist) { | ||
try { | ||
await clusterClient.indices.create({ | ||
index: concreteIndexName, | ||
|
@@ -135,8 +137,19 @@ export class RuleDataClient implements IRuleDataClient { | |
}, | ||
}); | ||
} catch (err) { | ||
// something might have created the index already, that sounds OK | ||
if (err?.meta?.body?.error?.type !== 'resource_already_exists_exception') { | ||
// If the index already exists and it's the write index for the alias, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the index already exists, will we ever get here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's still possible to get here if multiple code paths attempt to create the index at the same time. It can be triggered in testing if the index doesn't exist yet with a construct like
in this case both calls race to attempt to make the index and often one hits a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True. But if the lack of a write index was the cause for this function being called we'd just fail silently because There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see. I was counting on the bulk retry to fail again in that case and throw the error, but I didn't notice that it wasn't checking for errors on the retry. I added handling for both in 28722b1 - the retry bulk call now throws errors if it encounters any, and if |
||
// something else created it so suppress the error. If it's not the write | ||
// index, that's bad, throw an error. | ||
if (err?.meta?.body?.error?.type === 'resource_already_exists_exception') { | ||
const { body: existingIndices } = await clusterClient.indices.get({ | ||
index: concreteIndexName, | ||
}); | ||
if (!existingIndices[concreteIndexName]?.aliases?.[alias]?.is_write_index) { | ||
throw Error( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as of ES5 they're supposed to be equivalent, right? https://es5.github.io/#x15.11.1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but for a custom exception this might not work. Example: class CustomError extends Error {
constructor(value) {
super('Hello this is my message');
this.value = value;
}
}
let e = CustomError(42);
console.log(e.value); // ? Actually, I just checked it in Chrome, and calling However, this won't work the same way for any constructor defined as a function. In general, something like that would be needed: function User(name) {
if (!new.target) { // if you run me without new
return new User(name); // ...I will add new for you
}
this.name = name;
}
let john = User("John"); // redirects call to new User
alert(john.name); // John See https://javascript.info/constructor-new#constructor-mode-test-new-target Maybe I was overthinking when trying to explain it, but for me it's quite simple - let's use Sorry, I'm 🚲 🏠 🎨 'ing |
||
`Attempted to create index: ${concreteIndexName} as the write index for alias: ${alias}, but the index already exists and is not the write index for the alias` | ||
); | ||
} | ||
} else { | ||
throw err; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
{ | ||
"signal.ancestors.depth": "kibana.alert.ancestors.depth", | ||
"signal.ancestors.id": "kibana.alert.ancestors.id", | ||
"signal.ancestors.index": "kibana.alert.ancestors.index", | ||
"signal.ancestors.type": "kibana.alert.ancestors.type", | ||
"signal.depth": "kibana.alert.depth", | ||
"signal.original_event.action": "kibana.alert.original_event.action", | ||
"signal.original_event.category": "kibana.alert.original_event.category", | ||
"signal.original_event.code": "kibana.alert.original_event.code", | ||
"signal.original_event.created": "kibana.alert.original_event.created", | ||
"signal.original_event.dataset": "kibana.alert.original_event.dataset", | ||
"signal.original_event.duration": "kibana.alert.original_event.duration", | ||
"signal.original_event.end": "kibana.alert.original_event.end", | ||
"signal.original_event.hash": "kibana.alert.original_event.hash", | ||
"signal.original_event.id": "kibana.alert.original_event.id", | ||
"signal.original_event.kind": "kibana.alert.original_event.kind", | ||
"signal.original_event.module": "kibana.alert.original_event.module", | ||
"signal.original_event.outcome": "kibana.alert.original_event.outcome", | ||
"signal.original_event.provider": "kibana.alert.original_event.provider", | ||
"signal.original_event.risk_score": "kibana.alert.original_event.risk_score", | ||
"signal.original_event.risk_score_norm": "kibana.alert.original_event.risk_score_norm", | ||
"signal.original_event.sequence": "kibana.alert.original_event.sequence", | ||
"signal.original_event.severity": "kibana.alert.original_event.severity", | ||
"signal.original_event.start": "kibana.alert.original_event.start", | ||
"signal.original_event.timezone": "kibana.alert.original_event.timezone", | ||
"signal.original_event.type": "kibana.alert.original_event.type", | ||
"signal.original_time": "kibana.alert.original_time", | ||
"signal.rule.author": "kibana.alert.rule.author", | ||
"signal.rule.building_block_type": "kibana.alert.rule.building_block_type", | ||
"signal.rule.created_at": "kibana.alert.rule.created_at", | ||
"signal.rule.created_by": "kibana.alert.rule.created_by", | ||
"signal.rule.description": "kibana.alert.rule.description", | ||
"signal.rule.enabled": "kibana.alert.rule.enabled", | ||
"signal.rule.false_positives": "kibana.alert.rule.false_positives", | ||
"signal.rule.from": "kibana.alert.rule.from", | ||
"signal.rule.id": "kibana.alert.rule.id", | ||
"signal.rule.immutable": "kibana.alert.rule.immutable", | ||
"signal.rule.index": "kibana.alert.rule.index", | ||
"signal.rule.interval": "kibana.alert.rule.interval", | ||
"signal.rule.language": "kibana.alert.rule.language", | ||
"signal.rule.license": "kibana.alert.rule.license", | ||
"signal.rule.max_signals": "kibana.alert.rule.max_signals", | ||
"signal.rule.name": "kibana.alert.rule.name", | ||
"signal.rule.note": "kibana.alert.rule.note", | ||
"signal.rule.query": "kibana.alert.rule.query", | ||
"signal.rule.references": "kibana.alert.rule.references", | ||
"signal.rule.risk_score": "kibana.alert.risk_score", | ||
"signal.rule.risk_score_mapping.field": "kibana.alert.rule.risk_score_mapping.field", | ||
"signal.rule.risk_score_mapping.operator": "kibana.alert.rule.risk_score_mapping.operator", | ||
"signal.rule.risk_score_mapping.value": "kibana.alert.rule.risk_score_mapping.value", | ||
"signal.rule.rule_id": "kibana.alert.rule.rule_id", | ||
"signal.rule.rule_name_override": "kibana.alert.rule.rule_name_override", | ||
"signal.rule.saved_id": "kibana.alert.rule.saved_id", | ||
"signal.rule.severity": "kibana.alert.severity", | ||
"signal.rule.severity_mapping.field": "kibana.alert.rule.severity_mapping.field", | ||
"signal.rule.severity_mapping.operator": "kibana.alert.rule.severity_mapping.operator", | ||
"signal.rule.severity_mapping.value": "kibana.alert.rule.severity_mapping.value", | ||
"signal.rule.severity_mapping.severity": "kibana.alert.rule.severity_mapping.severity", | ||
"signal.rule.tags": "kibana.alert.rule.tags", | ||
"signal.rule.threat.framework": "kibana.alert.rule.threat.framework", | ||
"signal.rule.threat.tactic.id": "kibana.alert.rule.threat.tactic.id", | ||
"signal.rule.threat.tactic.name": "kibana.alert.rule.threat.tactic.name", | ||
"signal.rule.threat.tactic.reference": "kibana.alert.rule.threat.tactic.reference", | ||
"signal.rule.threat.technique.id": "kibana.alert.rule.threat.technique.id", | ||
"signal.rule.threat.technique.name": "kibana.alert.rule.threat.technique.name", | ||
"signal.rule.threat.technique.reference": "kibana.alert.rule.threat.technique.reference", | ||
"signal.rule.threat.technique.subtechnique.id": "kibana.alert.rule.threat.technique.subtechnique.id", | ||
"signal.rule.threat.technique.subtechnique.name": "kibana.alert.rule.threat.technique.subtechnique.name", | ||
"signal.rule.threat.technique.subtechnique.reference": "kibana.alert.rule.threat.technique.subtechnique.reference", | ||
"signal.rule.threat_index": "kibana.alert.rule.threat_index", | ||
"signal.rule.threat_indicator_path": "kibana.alert.rule.threat_indicator_path", | ||
"signal.rule.threat_language": "kibana.alert.rule.threat_language", | ||
"signal.rule.threat_mapping.entries.field": "kibana.alert.rule.threat_mapping.entries.field", | ||
"signal.rule.threat_mapping.entries.value": "kibana.alert.rule.threat_mapping.entries.value", | ||
"signal.rule.threat_mapping.entries.type": "kibana.alert.rule.threat_mapping.entries.type", | ||
"signal.rule.threat_query": "kibana.alert.rule.threat_query", | ||
"signal.rule.threshold.field": "kibana.alert.rule.threshold.field", | ||
"signal.rule.threshold.value": "kibana.alert.rule.threshold.value", | ||
"signal.rule.timeline_id": "kibana.alert.rule.timeline_id", | ||
"signal.rule.timeline_title": "kibana.alert.rule.timeline_title", | ||
"signal.rule.to": "kibana.alert.rule.to", | ||
"signal.rule.type": "kibana.alert.rule.type", | ||
"signal.rule.updated_at": "kibana.alert.rule.updated_at", | ||
"signal.rule.updated_by": "kibana.alert.rule.updated_by", | ||
"signal.rule.version": "kibana.alert.rule.version", | ||
"signal.status": "kibana.alert.workflow_status", | ||
"signal.threshold_result.from": "kibana.alert.threshold_result.from", | ||
"signal.threshold_result.terms.field": "kibana.alert.threshold_result.terms.field", | ||
"signal.threshold_result.terms.value": "kibana.alert.threshold_result.terms.value", | ||
"signal.threshold_result.cardinality.field": "kibana.alert.threshold_result.cardinality.field", | ||
"signal.threshold_result.cardinality.value": "kibana.alert.threshold_result.cardinality.value", | ||
"signal.threshold_result.count": "kibana.alert.threshold_result.count" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
illegal_argument_exception
is returned if the alias exists but has no write_index.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately
illegal_argument_exception
can be returned for many reasons other than the lack of a write index. I can imagine two ways to reduce the risk of misinterpretation:is_write_alias
.Do you think it's safe enough to just assume the reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point, I'll add logic similar to the old "alias exists" check in
createWriteTargetIfNeeded
but instead checking to ensure the alias has no write target before attempting to create a write target for it. You're right that otherwise we could end up withillegal_argument_exceptions
that cause us to try creating a new write target and that would cause problems if the alias already has a write target.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I replaced the
aliasExists
check with a more general "index exists" check on the AAD indices. This makes thecreateWriteTargetIfNeeded
function safer to call optimistically when we encounterindex_not_found
orillegal_argument
exceptions while still allowing the security solution to add AAD aliases to our.siem-signals
indices. Now, if we do get anillegal_argument_exception
for a reason other than "no concrete AAD indices exist", thencreateWriteTargetIfNeeded
will return without making new indices and potentially making things worse.