diff --git a/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java b/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java index e0a974740..73654d73c 100644 --- a/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java +++ b/src/main/java/org/opensearch/securityanalytics/SecurityAnalyticsPlugin.java @@ -39,9 +39,11 @@ import org.opensearch.securityanalytics.action.IndexDetectorAction; import org.opensearch.securityanalytics.action.SearchDetectorAction; import org.opensearch.securityanalytics.action.UpdateIndexMappingsAction; +import org.opensearch.securityanalytics.action.ValidateRulesAction; import org.opensearch.securityanalytics.mapper.MapperService; import org.opensearch.securityanalytics.resthandler.RestAcknowledgeAlertsAction; import org.opensearch.securityanalytics.resthandler.RestGetFindingsAction; +import org.opensearch.securityanalytics.resthandler.RestValidateRulesAction; import org.opensearch.securityanalytics.transport.TransportAcknowledgeAlertsAction; import org.opensearch.securityanalytics.transport.TransportCreateIndexMappingsAction; import org.opensearch.securityanalytics.transport.TransportGetFindingsAction; @@ -75,6 +77,7 @@ import org.opensearch.securityanalytics.transport.TransportGetMappingsViewAction; import org.opensearch.securityanalytics.transport.TransportIndexDetectorAction; import org.opensearch.securityanalytics.transport.TransportSearchDetectorAction; +import org.opensearch.securityanalytics.transport.TransportValidateRulesAction; import org.opensearch.securityanalytics.util.DetectorIndices; import org.opensearch.securityanalytics.util.RuleIndices; import org.opensearch.securityanalytics.util.RuleTopicIndices; @@ -140,7 +143,8 @@ public List getRestHandlers(Settings settings, new RestGetAlertsAction(), new RestIndexRuleAction(), new RestSearchRuleAction(), - new RestDeleteRuleAction() + new RestDeleteRuleAction(), + new RestValidateRulesAction() ); } @@ -176,7 +180,8 @@ public List> getSettings() { new ActionPlugin.ActionHandler<>(GetAlertsAction.INSTANCE, TransportGetAlertsAction.class), new ActionPlugin.ActionHandler<>(IndexRuleAction.INSTANCE, TransportIndexRuleAction.class), new ActionPlugin.ActionHandler<>(SearchRuleAction.INSTANCE, TransportSearchRuleAction.class), - new ActionPlugin.ActionHandler<>(DeleteRuleAction.INSTANCE, TransportDeleteRuleAction.class) + new ActionPlugin.ActionHandler<>(DeleteRuleAction.INSTANCE, TransportDeleteRuleAction.class), + new ActionPlugin.ActionHandler<>(ValidateRulesAction.INSTANCE, TransportValidateRulesAction.class) ); } } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesAction.java b/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesAction.java new file mode 100644 index 000000000..cde6ebac8 --- /dev/null +++ b/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesAction.java @@ -0,0 +1,19 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.action; + +import org.opensearch.action.ActionType; +import org.opensearch.action.support.master.AcknowledgedResponse; + +public class ValidateRulesAction extends ActionType{ + + public static final String NAME = "cluster:admin/opendistro/securityanalytics/rules/validate"; + public static final ValidateRulesAction INSTANCE = new ValidateRulesAction(); + + + public ValidateRulesAction() { + super(NAME, ValidateRulesResponse::new); + } +} diff --git a/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesRequest.java b/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesRequest.java new file mode 100644 index 000000000..6dc37faf4 --- /dev/null +++ b/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesRequest.java @@ -0,0 +1,136 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.action; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.common.Strings; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.XContentParserUtils; + + +import static org.opensearch.action.ValidateActions.addValidationError; + +public class ValidateRulesRequest extends ActionRequest implements ToXContentObject { + + private static final Logger log = LogManager.getLogger(ValidateRulesRequest.class); + + public static final String INDEX_NAME_FIELD = "index_name"; + public static final String RULES_FIELD = "rules"; + + String indexName; + List rules; + + public ValidateRulesRequest(String indexName, List rules) { + super(); + this.indexName = indexName; + this.rules = rules; + } + + public ValidateRulesRequest(StreamInput sin) throws IOException { + this( + sin.readString(), + sin.readStringList() + ); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (indexName == null || indexName.length() == 0) { + validationException = addValidationError(String.format(Locale.getDefault(), "%s is missing", INDEX_NAME_FIELD), validationException); + } + if (rules == null || rules.size() == 0) { + validationException = addValidationError(String.format(Locale.getDefault(), "%s are missing", RULES_FIELD), validationException); + } + return validationException; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(indexName); + out.writeStringCollection(rules); + } + + public static ValidateRulesRequest parse(XContentParser xcp) throws IOException { + String indexName = null; + List ruleIds = null; + + if (xcp.currentToken() == null) { + xcp.nextToken(); + } + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp); + while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { + String fieldName = xcp.currentName(); + xcp.nextToken(); + + switch (fieldName) { + case INDEX_NAME_FIELD: + indexName = xcp.text(); + break; + case RULES_FIELD: + ruleIds = new ArrayList<>(); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp); + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + ruleIds.add(xcp.text()); + } + break; + default: + xcp.skipChildren(); + } + } + return new ValidateRulesRequest(indexName, ruleIds); + } + + public ValidateRulesRequest indexName(String indexName) { + this.indexName = indexName; + return this; + } + + public ValidateRulesRequest rules(List rules) { + this.rules = rules; + return this; + } + + + + public String getIndexName() { + return this.indexName; + } + + public List getRules() { + return this.rules; + } + + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + public List setRules(List rules) { + return this.rules = rules; + } + + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field(INDEX_NAME_FIELD, indexName) + .field(RULES_FIELD, rules) + .endObject(); + } +} diff --git a/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesResponse.java b/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesResponse.java new file mode 100644 index 000000000..7403753cf --- /dev/null +++ b/src/main/java/org/opensearch/securityanalytics/action/ValidateRulesResponse.java @@ -0,0 +1,89 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.action; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.opensearch.action.ActionResponse; +import org.opensearch.common.Strings; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.xcontent.ToXContentObject; +import org.opensearch.common.xcontent.XContentBuilder; +import org.opensearch.securityanalytics.mapper.MapperUtils; + +public class ValidateRulesResponse extends ActionResponse implements ToXContentObject { + + public static final String NONAPPLICABLE_FIELDS = "nonapplicable_fields"; + + List nonapplicableFields; + + public ValidateRulesResponse(List nonapplicableFields) { + this.nonapplicableFields = nonapplicableFields; + } + + public ValidateRulesResponse(StreamInput in) throws IOException { + super(in); + nonapplicableFields = in.readStringList(); + int size = in.readVInt(); + if (size > 0) { + nonapplicableFields = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + nonapplicableFields.add(in.readString()); + } + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + if (nonapplicableFields != null) { + out.writeVInt(nonapplicableFields.size()); + for (String f : nonapplicableFields) { + out.writeString(f); + } + } else { + out.writeVInt(0); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (nonapplicableFields != null && nonapplicableFields.size() > 0) { + builder.field(NONAPPLICABLE_FIELDS, nonapplicableFields); + } + return builder.endObject(); + } + + public List getNonapplicableFields() { + return nonapplicableFields; + } + + @Override + public String toString() { + return Strings.toString(this); + } + + @Override + public int hashCode() { + return Objects.hash(new Object[]{this.nonapplicableFields}); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (getClass() != obj.getClass()) { + return false; + } + ValidateRulesResponse other = (ValidateRulesResponse) obj; + return this.nonapplicableFields.equals(other.nonapplicableFields); + } +} diff --git a/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java b/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java index 421693a9a..2b3389928 100644 --- a/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java +++ b/src/main/java/org/opensearch/securityanalytics/mapper/MapperUtils.java @@ -114,6 +114,51 @@ public static List validateIndexMappings(ImmutableOpenMap extractAllFieldsFlat(MappingMetadata mappingMetadata) { + MappingsTraverser mappingsTraverser = new MappingsTraverser(mappingMetadata); + List flatProperties = new ArrayList<>(); + // Setup + mappingsTraverser.addListener(new MappingsTraverser.MappingsTraverserListener() { + @Override + public void onLeafVisited(MappingsTraverser.Node node) { + flatProperties.add(node.currentPath); + } + + @Override + public void onError(String error) { + throw new IllegalArgumentException(error); + } + }); + // Do traverse + mappingsTraverser.traverse(); + return flatProperties; + } + + public static List extractAllFieldsFlat(Map mappingsMap) { + MappingsTraverser mappingsTraverser = new MappingsTraverser(mappingsMap, Set.of()); + List flatProperties = new ArrayList<>(); + // Setup + mappingsTraverser.addListener(new MappingsTraverser.MappingsTraverserListener() { + @Override + public void onLeafVisited(MappingsTraverser.Node node) { + flatProperties.add(node.currentPath); + } + + @Override + public void onError(String error) { + throw new IllegalArgumentException(error); + } + }); + // Do traverse + mappingsTraverser.traverse(); + return flatProperties; + } + public static List getAllNonAliasFieldsFromIndex(MappingMetadata mappingMetadata) { MappingsTraverser mappingsTraverser = new MappingsTraverser(mappingMetadata); return mappingsTraverser.extractFlatNonAliasFields(); diff --git a/src/main/java/org/opensearch/securityanalytics/model/Detector.java b/src/main/java/org/opensearch/securityanalytics/model/Detector.java index ecf655f78..4dffe6bd6 100644 --- a/src/main/java/org/opensearch/securityanalytics/model/Detector.java +++ b/src/main/java/org/opensearch/securityanalytics/model/Detector.java @@ -57,7 +57,7 @@ public class Detector implements Writeable, ToXContentObject { private static final String FINDINGS_INDEX = "findings_index"; private static final String FINDINGS_INDEX_PATTERN = "findings_index_pattern"; - public static final String DETECTORS_INDEX = ".opensearch-detectors-config"; + public static final String DETECTORS_INDEX = ".opensearch-sap-detectors-config"; public static final NamedXContentRegistry.Entry XCONTENT_REGISTRY = new NamedXContentRegistry.Entry( Detector.class, diff --git a/src/main/java/org/opensearch/securityanalytics/model/Rule.java b/src/main/java/org/opensearch/securityanalytics/model/Rule.java index d5c90d02e..5fa63eeef 100644 --- a/src/main/java/org/opensearch/securityanalytics/model/Rule.java +++ b/src/main/java/org/opensearch/securityanalytics/model/Rule.java @@ -50,10 +50,12 @@ public class Rule implements Writeable, ToXContentObject { public static final String STATUS = "status"; private static final String QUERIES = "queries"; + public static final String QUERY_FIELD_NAMES = "query_field_names"; + public static final String RULE = "rule"; - public static final String PRE_PACKAGED_RULES_INDEX = ".opensearch-pre-packaged-rules-config"; - public static final String CUSTOM_RULES_INDEX = ".opensearch-custom-rules-config"; + public static final String PRE_PACKAGED_RULES_INDEX = ".opensearch-sap-pre-packaged-rules-config"; + public static final String CUSTOM_RULES_INDEX = ".opensearch-sap-custom-rules-config"; public static final NamedXContentRegistry.Entry XCONTENT_REGISTRY = new NamedXContentRegistry.Entry( Rule.class, @@ -89,12 +91,14 @@ public class Rule implements Writeable, ToXContentObject { private List queries; + private List queryFieldNames; + private String rule; public Rule(String id, Long version, String title, String category, String logSource, String description, List references, List tags, String level, List falsePositives, String author, String status, Instant date, - List queries, String rule) { + List queries, List queryFieldNames, String rule) { this.id = id != null? id: NO_ID; this.version = version != null? version: NO_VERSION; @@ -115,11 +119,12 @@ public Rule(String id, Long version, String title, String category, String logSo this.date = date; this.queries = queries; + this.queryFieldNames = queryFieldNames; this.rule = rule; } public Rule(String id, Long version, SigmaRule rule, String category, - List queries, String original) { + List queries, List queryFieldNames, String original) { this( id, version, @@ -137,6 +142,7 @@ public Rule(String id, Long version, SigmaRule rule, String category, rule.getStatus().toString(), Instant.ofEpochMilli(rule.getDate().getTime()), queries.stream().map(Value::new).collect(Collectors.toList()), + queryFieldNames.stream().map(Value::new).collect(Collectors.toList()), original); } @@ -156,6 +162,7 @@ public Rule(StreamInput sin) throws IOException { sin.readString(), sin.readInstant(), sin.readList(Value::readFrom), + sin.readList(Value::readFrom), sin.readString()); } @@ -180,6 +187,8 @@ public void writeTo(StreamOutput out) throws IOException { out.writeInstant(date); out.writeCollection(queries); + out.writeCollection(queryFieldNames); + out.writeString(rule); } @@ -220,6 +229,9 @@ private XContentBuilder createXContentBuilder(XContentBuilder builder, ToXConten Value[] queryArray = new Value[]{}; queryArray = queries.toArray(queryArray); builder.field(QUERIES, queryArray); + Value[] queryFieldNamesArray = new Value[]{}; + queryFieldNamesArray = queryFieldNames.toArray(queryFieldNamesArray); + builder.field(QUERY_FIELD_NAMES, queryFieldNamesArray); builder.field(RULE, rule); if (params.paramAsBoolean("with_type", false)) { @@ -264,6 +276,7 @@ public static Rule parse(XContentParser xcp, String id, Long version) throws IOE Instant date = null; List queries = new ArrayList<>(); + List queryFields = new ArrayList<>(); String original = null; XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp); @@ -320,6 +333,12 @@ public static Rule parse(XContentParser xcp, String id, Long version) throws IOE queries.add(Value.parse(xcp)); } break; + case QUERY_FIELD_NAMES: + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp); + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + queryFields.add(Value.parse(xcp)); + } + break; case RULE: original = xcp.text(); break; @@ -343,6 +362,7 @@ public static Rule parse(XContentParser xcp, String id, Long version) throws IOE status, date, queries, + queryFields, Objects.requireNonNull(original, "Rule String is null") ); } @@ -418,4 +438,8 @@ public String getRule() { public List getQueries() { return queries; } + + public List getQueryFieldNames() { + return queryFieldNames; + } } \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/resthandler/RestValidateRulesAction.java b/src/main/java/org/opensearch/securityanalytics/resthandler/RestValidateRulesAction.java new file mode 100644 index 000000000..8e291d608 --- /dev/null +++ b/src/main/java/org/opensearch/securityanalytics/resthandler/RestValidateRulesAction.java @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.resthandler; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.node.NodeClient; +import org.opensearch.common.xcontent.ToXContent; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.XContentParserUtils; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestResponse; +import org.opensearch.rest.RestStatus; +import org.opensearch.rest.action.RestResponseListener; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; +import org.opensearch.securityanalytics.action.CreateIndexMappingsRequest; +import org.opensearch.securityanalytics.action.IndexRuleAction; +import org.opensearch.securityanalytics.action.IndexRuleRequest; +import org.opensearch.securityanalytics.action.IndexRuleResponse; +import org.opensearch.securityanalytics.action.ValidateRulesAction; +import org.opensearch.securityanalytics.action.ValidateRulesRequest; +import org.opensearch.securityanalytics.model.Detector; +import org.opensearch.securityanalytics.util.RestHandlerUtils; + +public class RestValidateRulesAction extends BaseRestHandler { + + private static final Logger log = LogManager.getLogger(RestValidateRulesAction.class); + + @Override + public String getName() { + return "validate_rules_action"; + } + + @Override + public List routes() { + return List.of( + new Route(RestRequest.Method.POST, SecurityAnalyticsPlugin.RULE_BASE_URI + "/validate") + ); + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + + + ValidateRulesRequest req; + try (XContentParser xcp = request.contentParser()) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp); + req = ValidateRulesRequest.parse(xcp); + } + return channel -> client.execute(ValidateRulesAction.INSTANCE, req, new RestToXContentListener<>(channel)); + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java index e7fc3dfb2..5eef8807d 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportIndexRuleAction.java @@ -4,6 +4,7 @@ */ package org.opensearch.securityanalytics.transport; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.search.join.ScoreMode; @@ -180,8 +181,13 @@ void prepareRuleIndexing() { final QueryBackend backend = new OSQueryBackend(category, true, true); List queries = backend.convertRule(parsedRule); - - Rule ruleDoc = new Rule(NO_ID, NO_VERSION, parsedRule, category, queries.stream().map(Object::toString).collect(Collectors.toList()), rule); + Set queryFieldNames = backend.getQueryFields().keySet(); + Rule ruleDoc = new Rule( + NO_ID, NO_VERSION, parsedRule, category, + queries.stream().map(Object::toString).collect(Collectors.toList()), + new ArrayList<>(queryFieldNames), + rule + ); indexRule(ruleDoc); } catch (IOException | SigmaError e) { onFailures(e); diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportValidateRulesAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportValidateRulesAction.java new file mode 100644 index 000000000..9290d8ed9 --- /dev/null +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportValidateRulesAction.java @@ -0,0 +1,60 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.transport; + +import java.util.List; +import org.opensearch.action.ActionListener; +import org.opensearch.action.StepListener; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.master.AcknowledgedResponse; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.securityanalytics.action.CreateIndexMappingsAction; +import org.opensearch.securityanalytics.action.CreateIndexMappingsRequest; +import org.opensearch.securityanalytics.action.ValidateRulesAction; +import org.opensearch.securityanalytics.action.ValidateRulesRequest; +import org.opensearch.securityanalytics.action.ValidateRulesResponse; +import org.opensearch.securityanalytics.mapper.MapperService; +import org.opensearch.securityanalytics.util.RuleIndices; +import org.opensearch.securityanalytics.util.RuleValidator; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +public class TransportValidateRulesAction extends HandledTransportAction { + + private final RuleValidator ruleValidator; + private final ClusterService clusterService; + + @Inject + public TransportValidateRulesAction( + TransportService transportService, + ActionFilters actionFilters, + ClusterService clusterService, + Client client, + NamedXContentRegistry namedXContentRegistry + ) { + super(ValidateRulesAction.NAME, transportService, actionFilters, ValidateRulesRequest::new); + this.clusterService = clusterService; + this.ruleValidator = new RuleValidator(client, namedXContentRegistry); + } + + @Override + protected void doExecute(Task task, ValidateRulesRequest request, ActionListener actionListener) { + IndexMetadata index = clusterService.state().metadata().index(request.getIndexName()); + if (index == null) { + actionListener.onFailure(new IllegalStateException("Could not find index [" + request.getIndexName() + "]")); + return; + } + StepListener> validateRulesResponseListener = new StepListener(); + validateRulesResponseListener.whenComplete(validateRulesResponse -> { + actionListener.onResponse(new ValidateRulesResponse(validateRulesResponse)); + }, actionListener::onFailure); + ruleValidator.validateCustomRules(request.getRules(), request.getIndexName(), validateRulesResponseListener); + } +} \ No newline at end of file diff --git a/src/main/java/org/opensearch/securityanalytics/util/RuleIndices.java b/src/main/java/org/opensearch/securityanalytics/util/RuleIndices.java index ad86f69c9..2e42b041d 100644 --- a/src/main/java/org/opensearch/securityanalytics/util/RuleIndices.java +++ b/src/main/java/org/opensearch/securityanalytics/util/RuleIndices.java @@ -4,12 +4,15 @@ */ package org.opensearch.securityanalytics.util; +import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchStatusException; import org.opensearch.action.ActionListener; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.admin.indices.mapping.get.GetMappingsRequest; +import org.opensearch.action.admin.indices.mapping.get.GetMappingsResponse; import org.opensearch.action.bulk.BulkRequest; import org.opensearch.action.bulk.BulkResponse; import org.opensearch.action.index.IndexRequest; @@ -21,6 +24,7 @@ import org.opensearch.cluster.ClusterState; import org.opensearch.cluster.health.ClusterIndexHealth; import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.MappingMetadata; import org.opensearch.cluster.routing.IndexRoutingTable; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.io.PathUtils; @@ -34,6 +38,7 @@ import org.opensearch.index.reindex.DeleteByQueryRequestBuilder; import org.opensearch.rest.RestStatus; import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.securityanalytics.mapper.MapperUtils; import org.opensearch.securityanalytics.model.Detector; import org.opensearch.securityanalytics.model.Rule; import org.opensearch.securityanalytics.rules.backend.OSQueryBackend; @@ -282,8 +287,14 @@ private List getQueries(QueryBackend backend, String category, List ruleQueries = backend.convertRule(rule); - - Rule ruleModel = new Rule(rule.getId().toString(), NO_VERSION, rule, category, ruleQueries.stream().map(Object::toString).collect(Collectors.toList()), ruleStr); + Set queryFieldNames = backend.getQueryFields().keySet(); + + Rule ruleModel = new Rule( + rule.getId().toString(), NO_VERSION, rule, category, + ruleQueries.stream().map(Object::toString).collect(Collectors.toList()), + new ArrayList<>(queryFieldNames), + ruleStr + ); queries.add(ruleModel); } return queries; diff --git a/src/main/java/org/opensearch/securityanalytics/util/RuleValidator.java b/src/main/java/org/opensearch/securityanalytics/util/RuleValidator.java new file mode 100644 index 000000000..0b53e9a35 --- /dev/null +++ b/src/main/java/org/opensearch/securityanalytics/util/RuleValidator.java @@ -0,0 +1,115 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import org.opensearch.action.ActionListener; +import org.opensearch.action.StepListener; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.search.fetch.subphase.FetchSourceContext; +import org.opensearch.securityanalytics.action.GetMappingsViewAction; +import org.opensearch.securityanalytics.action.GetMappingsViewRequest; +import org.opensearch.securityanalytics.action.GetMappingsViewResponse; +import org.opensearch.securityanalytics.action.SearchRuleAction; +import org.opensearch.securityanalytics.action.SearchRuleRequest; +import org.opensearch.securityanalytics.mapper.MapperUtils; +import org.opensearch.securityanalytics.model.Rule; + +public class RuleValidator +{ + private final static int MAX_RULES_TO_VALIDATE = 1000; + + private final static String RULE_ID = "_id"; + + private final Client client; + private final NamedXContentRegistry namedXContentRegistry; + + public RuleValidator(Client client, NamedXContentRegistry namedXContentRegistry) { + this.client = client; + this.namedXContentRegistry = namedXContentRegistry; + } + + public void validateCustomRules(List ruleIds, String indexName, ActionListener> listener) { + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.fetchSource(FetchSourceContext.FETCH_SOURCE); + + QueryBuilder queryBuilder = QueryBuilders.termsQuery( RULE_ID, ruleIds.toArray(new String[]{})); + SearchRequest searchRequest = new SearchRequest(Rule.CUSTOM_RULES_INDEX) + .source(new SearchSourceBuilder() + .seqNoAndPrimaryTerm(false) + .version(false) + .query(queryBuilder) + .fetchSource(FetchSourceContext.FETCH_SOURCE) + .size(MAX_RULES_TO_VALIDATE) + ) + .indices(Rule.CUSTOM_RULES_INDEX); + + StepListener searchRuleResponseListener = new StepListener(); + searchRuleResponseListener.whenComplete(searchRuleResponse -> { + + List rules = getRules(searchRuleResponse, namedXContentRegistry); + validateRules(rules, indexName, listener); + + }, listener::onFailure); + client.execute(SearchRuleAction.INSTANCE, new SearchRuleRequest(false, searchRequest), searchRuleResponseListener); + } + + private void validateRules(List rules, String indexName, ActionListener> listener) { + // Get index mappings + String ruleTopic = rules.get(0).getCategory(); + StepListener getMappingsViewResponseListener = new StepListener(); + getMappingsViewResponseListener.whenComplete(getMappingsViewResponse -> { + + List nonapplicableRuleIds = new ArrayList<>(); + for(Rule r : rules) { + // We will check against all index fields and applicable template aliases too + List allIndexFields = MapperUtils.extractAllFieldsFlat(getMappingsViewResponse.getAliasMappings()); + allIndexFields.addAll(getMappingsViewResponse.getUnmappedIndexFields()); + // check if all rule fields are present in index fields + List missingRuleFields = r.getQueryFieldNames() + .stream() + .map(e -> e.getValue()) + .filter(e -> allIndexFields.contains(e) == false) + .collect(Collectors.toList()); + + if (missingRuleFields.size() > 0) { + nonapplicableRuleIds.add(r.getId()); + } + } + listener.onResponse(nonapplicableRuleIds); + }, listener::onFailure); + client.execute( + GetMappingsViewAction.INSTANCE, + new GetMappingsViewRequest(indexName, ruleTopic), + getMappingsViewResponseListener + ); + } + + public static List getRules(SearchResponse response, NamedXContentRegistry xContentRegistry) throws IOException { + List rules = new ArrayList<>((int) response.getHits().getTotalHits().value); + for (SearchHit hit : response.getHits()) { + XContentParser xcp = XContentType.JSON.xContent().createParser( + xContentRegistry, + LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()); + Rule rule = Rule.docParse(xcp, hit.getId(), hit.getVersion()); + rules.add(rule); + } + return rules; + } +} diff --git a/src/main/resources/mappings/rules.json b/src/main/resources/mappings/rules.json index 50651c39a..39b9621d0 100644 --- a/src/main/resources/mappings/rules.json +++ b/src/main/resources/mappings/rules.json @@ -100,6 +100,15 @@ } } }, + "query_fields": { + "type": "nested", + "properties": { + "value": { + "type": "keyword", + "ignore_above": 256 + } + } + }, "rule": { "type": "text", "fields": { diff --git a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java index a3511b862..abb655156 100644 --- a/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java +++ b/src/test/java/org/opensearch/securityanalytics/SecurityAnalyticsRestTestCase.java @@ -4,8 +4,11 @@ */ package org.opensearch.securityanalytics; +import java.io.File; +import java.nio.file.Path; import org.apache.http.Header; import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.message.BasicHeader; @@ -30,6 +33,7 @@ import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentParser; import org.opensearch.common.xcontent.XContentParserUtils; +import org.opensearch.common.xcontent.XContentType; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.commons.alerting.model.ScheduledJob; import org.opensearch.commons.alerting.util.IndexUtilsKt; @@ -38,8 +42,11 @@ import org.opensearch.search.SearchHit; import org.opensearch.securityanalytics.action.CreateIndexMappingsRequest; import org.opensearch.securityanalytics.action.UpdateIndexMappingsRequest; +import org.opensearch.securityanalytics.config.monitors.DetectorMonitorConfig; import org.opensearch.securityanalytics.model.Detector; import org.opensearch.securityanalytics.model.Rule; +import org.opensearch.securityanalytics.util.DetectorIndices; +import org.opensearch.securityanalytics.util.RuleTopicIndices; import org.opensearch.test.rest.OpenSearchRestTestCase; import java.io.IOException; @@ -52,9 +59,42 @@ import java.util.stream.Collectors; import static org.opensearch.action.admin.indices.create.CreateIndexRequest.MAPPINGS; +import static org.opensearch.securityanalytics.util.RuleTopicIndices.ruleTopicIndexMappings; +import static org.opensearch.securityanalytics.util.RuleTopicIndices.ruleTopicIndexSettings; public class SecurityAnalyticsRestTestCase extends OpenSearchRestTestCase { + protected void createRuleTopicIndex(String detectorType, String additionalMapping) throws IOException { + + String mappings = "" + + " \"_meta\": {" + + " \"schema_version\": 1" + + " }," + + " \"properties\": {" + + " \"query\": {" + + " \"type\": \"percolator_ext\"" + + " }," + + " \"monitor_id\": {" + + " \"type\": \"text\"" + + " }," + + " \"index\": {" + + " \"type\": \"text\"" + + " }" + + " }"; + + String indexName = DetectorMonitorConfig.getRuleIndex(detectorType); + createIndex( + indexName, + Settings.builder().loadFromSource(ruleTopicIndexSettings(), XContentType.JSON).build(), + mappings + ); + // Update mappings + if (additionalMapping != null) { + Response response = makeRequest(client(), "PUT", indexName + "/_mapping", Collections.emptyMap(), new StringEntity(additionalMapping), new BasicHeader("Content-Type", "application/json")); + assertEquals(RestStatus.OK, restStatus(response)); + } + } + protected String createTestIndex(String index, String mapping) throws IOException { createTestIndex(index, mapping, Settings.EMPTY); return index; @@ -847,4 +887,41 @@ private String alertingScheduledJobMappings() { " }\n" + " }"; } + + protected void createNetflowLogIndex(String indexName) throws IOException { + String indexMapping = + " \"properties\": {" + + " \"netflow.source_ipv4_address\": {" + + " \"type\": \"ip\"" + + " }," + + " \"netflow.destination_transport_port\": {" + + " \"type\": \"integer\"" + + " }," + + " \"netflow.destination_ipv4_address\": {" + + " \"type\": \"ip\"" + + " }," + + " \"netflow.source_transport_port\": {" + + " \"type\": \"integer\"" + + " }" + + " }"; + + createIndex(indexName, Settings.EMPTY, indexMapping); + + // Insert sample doc + String sampleDoc = "{" + + " \"netflow.source_ipv4_address\":\"10.50.221.10\"," + + " \"netflow.destination_transport_port\":1234," + + " \"netflow.destination_ipv4_address\":\"10.53.111.14\"," + + " \"netflow.source_transport_port\":4444" + + "}"; + + // Index doc + Request indexRequest = new Request("POST", indexName + "/_doc?refresh=wait_for"); + indexRequest.setJsonEntity(sampleDoc); + Response response = client().performRequest(indexRequest); + assertEquals(HttpStatus.SC_CREATED, response.getStatusLine().getStatusCode()); + // Refresh everything + response = client().performRequest(new Request("POST", "_refresh")); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + } } \ No newline at end of file diff --git a/src/test/java/org/opensearch/securityanalytics/action/ValidateRulesRequestTests.java b/src/test/java/org/opensearch/securityanalytics/action/ValidateRulesRequestTests.java new file mode 100644 index 000000000..338b21f57 --- /dev/null +++ b/src/test/java/org/opensearch/securityanalytics/action/ValidateRulesRequestTests.java @@ -0,0 +1,58 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.action; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import org.junit.Assert; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.XContentParserUtils; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.rest.RestRequest; +import org.opensearch.test.OpenSearchTestCase; + + +import static org.opensearch.securityanalytics.TestHelpers.randomDetector; + +public class ValidateRulesRequestTests extends OpenSearchTestCase { + + public void testValidateRulesRequest_parseXContent() throws IOException { + + String source = "{" + + "\"index_name\": \"my_index_111\"," + + "\"rules\": [ \"rule_id_1\", \"rule_id_2\" ]" + + "}"; + ValidateRulesRequest req; + try (XContentParser xcp = createParser(JsonXContent.jsonXContent, source)) { + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.nextToken(), xcp); + req = ValidateRulesRequest.parse(xcp); + } + assertEquals("my_index_111", req.getIndexName()); + assertEquals(2, req.getRules().size()); + assertEquals("rule_id_1", req.getRules().get(0)); + assertEquals("rule_id_2", req.getRules().get(1)); + } + + public void testValidateRulesRequest_streams() throws IOException { + String indeName = "my_index_1"; + ValidateRulesRequest request = new ValidateRulesRequest(indeName, List.of("rule_id_1", "rule_id_2")); + Assert.assertNotNull(request); + + BytesStreamOutput out = new BytesStreamOutput(); + request.writeTo(out); + + StreamInput sin = StreamInput.wrap(out.bytes().toBytesRef().bytes); + ValidateRulesRequest newRequest = new ValidateRulesRequest(sin); + assertEquals(indeName, newRequest.getIndexName()); + assertEquals(2, newRequest.getRules().size()); + assertEquals("rule_id_1", newRequest.getRules().get(0)); + assertEquals("rule_id_2", newRequest.getRules().get(1)); + } + +} diff --git a/src/test/java/org/opensearch/securityanalytics/action/ValidateRulesResponseTests.java b/src/test/java/org/opensearch/securityanalytics/action/ValidateRulesResponseTests.java new file mode 100644 index 000000000..575c96b4a --- /dev/null +++ b/src/test/java/org/opensearch/securityanalytics/action/ValidateRulesResponseTests.java @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.securityanalytics.action; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.opensearch.common.bytes.BytesReference; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.xcontent.ToXContent; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentParser; +import org.opensearch.common.xcontent.XContentParserUtils; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.test.OpenSearchTestCase; + + +import static org.opensearch.securityanalytics.action.ValidateRulesRequest.RULES_FIELD; +import static org.opensearch.securityanalytics.action.ValidateRulesResponse.NONAPPLICABLE_FIELDS; + +public class ValidateRulesResponseTests extends OpenSearchTestCase { + + public void testValidateRulesResponse_parseXContent() throws IOException { + + ValidateRulesResponse response = new ValidateRulesResponse(List.of("rule_id_1")); + BytesReference bytes = BytesReference.bytes(response.toXContent(XContentFactory.jsonBuilder(), ToXContent.EMPTY_PARAMS)); + try (XContentParser xcp = createParser(JsonXContent.jsonXContent, bytes)) { + if (xcp.currentToken() == null) { + xcp.nextToken(); + } + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, xcp.currentToken(), xcp); + List ruleIds = null; + while (xcp.nextToken() != XContentParser.Token.END_OBJECT) { + String fieldName = xcp.currentName(); + xcp.nextToken(); + assertEquals(NONAPPLICABLE_FIELDS, fieldName); + ruleIds = new ArrayList<>(); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, xcp.currentToken(), xcp); + while (xcp.nextToken() != XContentParser.Token.END_ARRAY) { + ruleIds.add(xcp.text()); + } + } + assertEquals(1, ruleIds.size()); + assertEquals("rule_id_1", ruleIds.get(0)); + } + } + + public void testValidateRulesResponse_streams() throws IOException { + ValidateRulesResponse response = new ValidateRulesResponse(List.of("rule_id_1", "rule_id_2")); + Assert.assertNotNull(response); + + BytesStreamOutput out = new BytesStreamOutput(); + response.writeTo(out); + + StreamInput sin = StreamInput.wrap(out.bytes().toBytesRef().bytes); + ValidateRulesResponse newResponse = new ValidateRulesResponse(sin); + assertEquals(2, newResponse.getNonapplicableFields().size()); + assertEquals("rule_id_1", newResponse.getNonapplicableFields().get(0)); + assertEquals("rule_id_2", newResponse.getNonapplicableFields().get(1)); + } + +} diff --git a/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java b/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java index c019363dd..ee274e0ce 100644 --- a/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java +++ b/src/test/java/org/opensearch/securityanalytics/findings/FindingIT.java @@ -132,7 +132,7 @@ public void testGetFindings_byDetectorType_oneDetector_success() throws IOExcept Assert.assertEquals(5, noOfSigmaRuleMatches); // Call GetFindings API Map params = new HashMap<>(); - params.put("detectorType", detector.getDetectorType().toUpperCase()); + params.put("detectorType", detector.getDetectorType()); Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); Map getFindingsBody = entityAsMap(getFindingsResponse); Assert.assertEquals(1, getFindingsBody.get("total_findings")); @@ -232,13 +232,13 @@ public void testGetFindings_byDetectorType_success() throws IOException { // Call GetFindings API for first detector Map params = new HashMap<>(); - params.put("detectorType", detector1.getDetectorType().toUpperCase()); + params.put("detectorType", detector1.getDetectorType()); Response getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); Map getFindingsBody = entityAsMap(getFindingsResponse); Assert.assertEquals(1, getFindingsBody.get("total_findings")); // Call GetFindings API for second detector params.clear(); - params.put("detectorType", detector2.getDetectorType().toUpperCase()); + params.put("detectorType", detector2.getDetectorType()); getFindingsResponse = makeRequest(client(), "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); getFindingsBody = entityAsMap(getFindingsResponse); Assert.assertEquals(1, getFindingsBody.get("total_findings")); diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java index 4266e15ca..f97e21a11 100644 --- a/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/DetectorRestApiIT.java @@ -158,7 +158,7 @@ public void testSearchingDetectors() throws IOException { Map searchResponseTotal = (Map) searchResponseHits.get("total"); Assert.assertEquals(1, searchResponseTotal.get("value")); } - + @SuppressWarnings("unchecked") public void testCreatingADetectorWithCustomRules() throws IOException { String index = createTestIndex(randomIndex(), windowsIndexMapping()); diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java index 74ed31f94..cd130c94e 100644 --- a/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/RuleRestApiIT.java @@ -15,6 +15,7 @@ import org.opensearch.search.SearchHit; import org.opensearch.securityanalytics.SecurityAnalyticsPlugin; import org.opensearch.securityanalytics.SecurityAnalyticsRestTestCase; +import org.opensearch.securityanalytics.action.ValidateRulesRequest; import org.opensearch.securityanalytics.config.monitors.DetectorMonitorConfig; import org.opensearch.securityanalytics.model.Detector; import org.opensearch.securityanalytics.model.DetectorInput; @@ -545,4 +546,97 @@ public void testDeletingUsedRule() throws IOException { hits = executeSearch(index, request); Assert.assertEquals(0, hits.size()); } + + public void testCustomRuleValidation() throws IOException { + String rule1 = "title: Remote Encrypting File System Abuse\n" + + "id: 5f92fff9-82e2-48eb-8fc1-8b133556a551\n" + + "description: Detects remote RPC calls to possibly abuse remote encryption service via MS-EFSR\n" + + "references:\n" + + " - https://attack.mitre.org/tactics/TA0008/\n" + + " - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-36942\n" + + " - https://github.com/jsecurity101/MSRPC-to-ATTACK/blob/main/documents/MS-EFSR.md\n" + + " - https://github.com/zeronetworks/rpcfirewall\n" + + " - https://zeronetworks.com/blog/stopping_lateral_movement_via_the_rpc_firewall/\n" + + "tags:\n" + + " - attack.defense_evasion\n" + + "status: experimental\n" + + "author: Sagie Dulce, Dekel Paz\n" + + "date: 2022/01/01\n" + + "modified: 2022/01/01\n" + + "logsource:\n" + + " product: rpc_firewall\n" + + " category: application\n" + + " definition: 'Requirements: install and apply the RPC Firewall to all processes with \"audit:true action:block uuid:df1941c5-fe89-4e79-bf10-463657acf44d or c681d488-d850-11d0-8c52-00c04fd90f7e'\n" + + "detection:\n" + + " selection:\n" + + " EventID: 22\n" + + " condition: selection\n" + + "falsepositives:\n" + + " - Legitimate usage of remote file encryption\n" + + "level: high"; + + String rule2 = "title: Remote Encrypting File System Abuse\n" + + "id: 5f92fff9-82e2-48eb-8fc1-8b133556a551\n" + + "description: Detects remote RPC calls to possibly abuse remote encryption service via MS-EFSR\n" + + "references:\n" + + " - https://attack.mitre.org/tactics/TA0008/\n" + + " - https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-36942\n" + + " - https://github.com/jsecurity101/MSRPC-to-ATTACK/blob/main/documents/MS-EFSR.md\n" + + " - https://github.com/zeronetworks/rpcfirewall\n" + + " - https://zeronetworks.com/blog/stopping_lateral_movement_via_the_rpc_firewall/\n" + + "tags:\n" + + " - attack.defense_evasion\n" + + "status: experimental\n" + + "author: Sagie Dulce, Dekel Paz\n" + + "date: 2022/01/01\n" + + "modified: 2022/01/01\n" + + "logsource:\n" + + " product: rpc_firewall\n" + + " category: application\n" + + " definition: 'Requirements: install and apply the RPC Firewall to all processes with \"audit:true action:block uuid:df1941c5-fe89-4e79-bf10-463657acf44d or c681d488-d850-11d0-8c52-00c04fd90f7e'\n" + + "detection:\n" + + " selection:\n" + + " EventID123: 22\n" + + " condition: selection\n" + + "falsepositives:\n" + + " - Legitimate usage of remote file encryption\n" + + "level: high"; + + // Create rule #1 + Response createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + new StringEntity(rule1), new BasicHeader("Content-Type", "application/json")); + Assert.assertEquals("Create rule failed", RestStatus.CREATED, restStatus(createResponse)); + + Map responseBody = asMap(createResponse); + + String rule1createdId = responseBody.get("_id").toString(); + int createdVersion = Integer.parseInt(responseBody.get("_version").toString()); + Assert.assertNotEquals("response is missing Id", Detector.NO_ID, rule1createdId); + Assert.assertTrue("incorrect version", createdVersion > 0); + Assert.assertEquals("Incorrect Location header", String.format(Locale.getDefault(), "%s/%s", SecurityAnalyticsPlugin.RULE_BASE_URI, rule1createdId), createResponse.getHeader("Location")); + // Create rule #2 + createResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI, Collections.singletonMap("category", "windows"), + new StringEntity(rule2), new BasicHeader("Content-Type", "application/json")); + Assert.assertEquals("Create rule failed", RestStatus.CREATED, restStatus(createResponse)); + + responseBody = asMap(createResponse); + + String rule2createdId = responseBody.get("_id").toString(); + createdVersion = Integer.parseInt(responseBody.get("_version").toString()); + Assert.assertNotEquals("response is missing Id", Detector.NO_ID, rule2createdId); + Assert.assertTrue("incorrect version", createdVersion > 0); + Assert.assertEquals("Incorrect Location header", String.format(Locale.getDefault(), "%s/%s", SecurityAnalyticsPlugin.RULE_BASE_URI, rule2createdId), createResponse.getHeader("Location")); + + // Create logIndex + createTestIndex("log_index_123", windowsIndexMapping()); + String validateRulesRequest = "{" + + "\"index_name\": \"log_index_123\"," + + "\"rules\": [\"" + rule1createdId + "\",\"" + rule2createdId + "\"]" + + "}"; + Response validationResponse = makeRequest(client(), "POST", SecurityAnalyticsPlugin.RULE_BASE_URI + "/validate", Collections.EMPTY_MAP, new StringEntity(validateRulesRequest), new BasicHeader("Content-Type", "application/json")); + responseBody = asMap(validationResponse); + assertTrue(responseBody.containsKey("nonapplicable_fields")); + assertEquals(rule2createdId, ((List)responseBody.get("nonapplicable_fields")).get(0)); + } + } \ No newline at end of file