-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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] Adds pre-packaged rule updates through the "Prebuilt Security Detection Rules" Fleet integration #96698
[Security] Adds pre-packaged rule updates through the "Prebuilt Security Detection Rules" Fleet integration #96698
Conversation
Pinging @elastic/security-detections-response (Team:Detections and Resp) |
response.saved_objects.forEach((so) => responses.push(so as IRuleAssetSavedObject)); | ||
if (response.total === 0) { | ||
break; | ||
} |
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.
If this total is zero, won't this exit the loop automatically? Just curious, I have used this newer API before. I couldn't tell from the docs.
In other parts of the code base for example I see:
const ruleUuids: string[] = [];
for await (const response of pitFinder.find()) {
ruleUuids.push(...response.saved_objects.map((object) => object.id));
}
await pitFinder.close();
return ruleUuids;
Something like that where they're not looking for totals and just looping until done and then doing a close.
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.
good question. i couldn't tell either. any guess who we should ping to double check?
type: ruleAssetSavedObjectType, | ||
}); | ||
const responses: IRuleAssetSavedObject[] = []; | ||
for await (const response of finder.find()) { |
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.
bummer the .find
doesn't take a template input and forces you to do that so as IRuleAssetSavedObject
below.
// check the rules installed via fleet and create/update if the version is newer | ||
const fleetRules = await getFleetInstalledRules(client); | ||
const fleetUpdates = fleetRules.filter( | ||
// @ts-expect-error data ruleMap.get() is safe because of ruleMap.has() check |
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.
fwiw, @ts-expect-error
count outside of test files is increasing per release when it should be be decreasing or staying steady or rising only slightly.
Maintainers, operators, and general outsiders from security_solutions who are cutting across our solution pepper them in and then later I think that creates the situation where it feels like the norm to whip them out and use them as if it is a normal typescript thing when in reality they're considered a code smell and something that should be fixed soon if possible.
Granted in a few situations where typescript is very wrong or we have to do an import of JS it is something we cannot avoid. However, we should avoid them altogether even if typescript is slightly wrong but we can work around typescript.
Can we can this code for example to be this:
const fleetUpdates = fleetRules.filter((r) => {
const ruleId = ruleMap.get(r.rule_id);
return ruleId == null || ruleId.version < r.version;
});
Then that removes the ts-expect-error
.
For example, a maintainer can now change that code to have accidental character addition like this below and if it passes the unit tests it will end up shipping broken.
const fleetUpdates = fleetRules.filter(
// @ts-expect-error data ruleMap.get() is safe because of ruleMap.has() check
(r) => !ruleMap_extra_chars.has(r.rule_id) || ruleMap.get(r.rule_id).version < r.version
);
Even in tests I try to avoid them if I can. My scale of good to bad goes like this:
- When possible use regular typescript/javascript, including generics, templates, etc...
- Ok, I have to work a bit around typescript with a change in code, but I still get type safety.
- Ok, if I have to use an "as cast" like "as " I will, but I tried to avoid it.
- Ok, I have to add a bang, "!" to turn something off. I really tried not to and don't want to but it's needed. I did try to use
?
or another technique but failed. - Ok, if I have to use an "as unknown as case", things are getting worse, but still not too bad, and the alternatives are worse than just doing it in the code.
- Finally, I really have no choice here, I must use a ts-expect-error and hope that it goes away very soon. I understand it's easy to break things :-( in this area of code by accident.
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.
thanks for the explanation, that's good to understand more of your thought process and approach!
i am definitely not a fan of ts-expect-error
style suppression for a whole line either. especially since a future error could be introduced and it's already suppressed. that's not good.
in this case, it seems the typesystem was unable to infer how has
and get
relate to each other, so even though what i did looks safe it guessed wrong.
storing .get()
in a variable seems like just the right solution. you can explicitly check that against null
, and then the other check it knows is non-null. i'm trying that change out locally now and running through the type checking
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.
Is == null
or === undefined
more idiomatic in this case?
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.
updated in 3df98ad (for == null
)
x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_prepackaged_rules.ts
Outdated
Show resolved
Hide resolved
const fleetResponse = await client.all(); | ||
const fleetRules = fleetResponse.map( | ||
// @ts-expect-error data is too loosely typed | ||
(so) => so.attributes as AddPrepackagedRulesSchema |
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 think you're shoe horning a bit here and this isn't the validateAllPrepackagedRules
as it is written is really what you want maybe.
The ts-expect-error
kind of gives it away as a bit as maybe something doesn't seem right.
If validateAllPrepackagedRules
throws if the rule is not validated and was written when rules on a file system are a thing. Is that what you want? You want a throw here if it is not valid? Also it will say in the error message:
within the folder rules/prepackaged_rules
I don't know if that's what you want either. The best bet would be to do something like this where you add a new function that is specific to checking and validating and throwing a specific error:
export const validateAllPrepackagedRulesFromSavedObjects = (
rules: Array<Array<SavedObject<IRuleAssetSOAttributes & SavedObjectAttributes>>>
): AddPrepackagedRulesSchemaDecoded[] => {
return rules.map((rule) => {
const decoded = addPrepackagedRulesSchema.decode(rule);
const checked = exactCheck(rule, decoded);
const onLeft = (errors: t.Errors): AddPrepackagedRulesSchemaDecoded => {
const ruleName = rule[0].attributes.name ? rule[0].attributes.name : '(rule name unknown)';
const ruleId = rule[0].attributes.rule_id
? rule[0].attributes.rule_id
: '(rule rule_id unknown)';
throw new BadRequestError(
`name: "${ruleName}", rule_id: "${ruleId}" within the saved object rules/prepackaged_rules ` +
`is not a valid detection engine rule. Expect the system ` +
`to not work with pre-packaged rules until this rule is fixed ` +
`or the file is removed. Error is: ${formatErrors(
errors
).join()}, Full rule contents are:\n${JSON.stringify(rule, null, 2)}`
);
};
const onRight = (schema: AddPrepackagedRulesSchema): AddPrepackagedRulesSchemaDecoded => {
return schema as AddPrepackagedRulesSchemaDecoded;
};
return pipe(checked, fold(onLeft, onRight));
});
};
/**
* Retrieve and validate rules that were installed from Fleet as saved objects.
*/
export const getFleetInstalledRules = async (
client: RuleAssetSavedObjectsClient
): Promise<AddPrepackagedRulesSchemaDecoded[]> => {
const fleetResponse = await client.all();
const fleetRules = fleetResponse.map((so) => so.attributes);
return validateAllPrepackagedRulesFromSavedObjects(fleetRules);
};
Then you can remove the ts-expect-error
but also look at that bit of weirdness where I have two arrays?
const ruleName = rule[0].attributes.name ? rule[0].attributes.name : '(rule name unknown)';
const ruleId = rule[0].attributes.rule_id
? rule[0].attributes.rule_id
: '(rule rule_id unknown)';
I don't think even that is 100% but it's closer to your intent of validating and then casting the data from a SO key/value generic data structure into a more non-generic data structure which has guaranteed fields such as the io-ts ones. I think maybe there just needs to be a loop over the inner array or something else is possibly off, but it does show at least progress here to where restructuring with a new function would be better.
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.
yeah, fair enough to make a separate function to validate here, so we don't assume any more than absolutely necessary about the incoming dynamically typed data.
I definitely need to do this check, I just mistakenly assumed that the validateAllPrepackagedRules
would also be appropriate here.
const decoded = addPrepackagedRulesSchema.decode(rule);
const checked = exactCheck(rule, decoded);
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.
x-pack/plugins/security_solution/server/lib/detection_engine/rules/constants.ts
Outdated
Show resolved
Hide resolved
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.
LGTM, thanks for all the nice updates and removal of ts-expect-errors! Code looks great here.
@elasticmachine merge upstream |
💚 Build Succeeded
Metrics [docs]
History
To update your PR or re-run it, just comment with: |
…ity Detection Rules" Fleet integration (elastic#96698) * Make the prepackaged rules functions async * Fix type for getPrepackagedRules mock * Install updates from saved objects & FS * Mock getLatestPrepackagedRules instead of getPrepackagedRules * Cleanup ruleAssetSavedObjectsClientFactory.all * Fix comment for "most recent version" * Switch to ruleMap.get() for less typescript errors * Remove unneeded constants * Fix SO.attributes sig and use custom validation
💚 Backport successful
This backport PR will be merged automatically after passing CI. |
…ity Detection Rules" Fleet integration (#96698) (#97154) * Make the prepackaged rules functions async * Fix type for getPrepackagedRules mock * Install updates from saved objects & FS * Mock getLatestPrepackagedRules instead of getPrepackagedRules * Cleanup ruleAssetSavedObjectsClientFactory.all * Fix comment for "most recent version" * Switch to ruleMap.get() for less typescript errors * Remove unneeded constants * Fix SO.attributes sig and use custom validation Co-authored-by: Ross Wolf <31489089+rw-access@users.noreply.github.com>
Summary
Related to https://github.com/elastic/security-team/issues/17
Rewrite of PR #91949 to use
security-rule
saved object instead of manual JSON.This allows for the prepackaged/builtin rules for the Detection Engine to be updated via Fleet. When installing the Fleet integration, rules are installed as the
security-rule
SO. In this PR, we automatically install the integration if it's available. From there, we can look at thesecurity-rule
SOs, union them with the prepackaged rules that are compiled in via JSON, and take the latest version of each. That builds the complete set of rules that users can install.PR dependencies were
Release note
UX
Some of these screenshots might be from mixed runs and have inconsistent timestamps, but I confirmed the end results.
Install rules from FS on a fresh install
Enable the "Prebuilt Security Detection Rules Integration"
Go back to the detections page. An update is recognized! Note: the banner doesn't mention that there are new rules, it only counts updates to existing rules
Install the rules. One was updated "(updated) Attempt to modify an Okta Policy" , and one was new: "New Rule from Fleet." Under this current design, rules are never deleted.
Next Steps
It's a little odd that Fleet installs the integration to a policy. Opened #96525 to discuss that. We could hide the package until there's a workflow. (
internal: true
in the manifest?)There's no logic in place to automatically download and install the
security_detection_engine
package behind the scenes. It's still a net win regardless. I plan to make a follow-up PR to automatically install that package.Probably should write some mocks? Could use a hand for the ideal approach.
Checklist
Delete any items that are not applicable to this PR.
For maintainers