From 458cd0371477f2e9b18e42470a36684894c3dcdb Mon Sep 17 00:00:00 2001 From: Nick Angelou Date: Tue, 8 Dec 2020 18:20:46 -0600 Subject: [PATCH] Policy alert storage (#2105) * wip: adding in ddb * fix: env * fix: add table definition * fix: hardcode table name * fix: remove unused env vars * fix: add missing alert type enum * Policy type * fix: delivery status for policy types * wip: alerting resourceTypes, resourceId * fix: typo in attribute name * feat: add sourceId * fix: utility function to summary needed sourceId * fix: revert FE changes * fix: revert FE changes * fix: order imports * fix: revert FE changes * fix: remove more FE changes * refactor: policy attributes for alerts * fix: required * chore: arrange attributes * fix: remove unused conts * fix: graphql had duplicate entries * fix: revert snapshot * mage gen fmt * fix: convert timestampt to UTC for CI * fix: ddb constraint * spelling of attribute * wip testign * mage gen fmt * fix: tests * feat: sorting * pr fixes * fix: filter by type * fix: alert filtering by nameContains * fix: remove unused query variables * move init function * fix: remove required fields * feat: add get alert (policy) test * fix: make optional graphql fields * pr fixes * mage gen fmt * fix: move alerts models * mage gen fmt * fix: remove unused var * feat: add alert detection union * fix: appsync typos * fix: failing tests due to Alert migration * fix: rename 'type' -> 'types' * fix: appsync template * fix: logtypes slice * fix: pr feedback * rename *IntegrationID -> *SourceID * fix: check for policyId as ruleId is always set * fix: appsync check alert type * fix: check for alert type in appsync * fix: update alert statuses Co-authored-by: panther-bot Co-authored-by: Aggelos Arvanitakis --- api/graphql/schema.graphql | 46 ++++-- api/lambda/alerts/models/api.go | 43 ++--- api/lambda/delivery/models/api.go | 13 +- deployments/appsync.yml | 88 ++++++++++- deployments/cloud_security.yml | 19 ++- go.mod | 2 +- .../alert_forwarder/forwarder/forwarder.go | 118 +++++++++++--- .../forwarder/forwarder_test.go | 112 +++++++++---- .../alert_forwarder/forwarder/metrics.go | 48 ++++++ .../compliance/alert_forwarder/main/config.go | 49 ++++++ .../compliance/alert_forwarder/main/lambda.go | 42 ++++- .../compliance/alert_processor/models/api.go | 3 + .../alert_processor/processor/processor.go | 14 ++ .../processor/processor_test.go | 29 +++- .../resource_processor/processor/handler.go | 8 +- .../alert_delivery/api/update_delivery.go | 6 - .../api/update_delivery_test.go | 35 +--- .../alert_forwarder/forwarder/forwarder.go | 43 ++--- .../forwarder/forwarder_test.go | 34 ++-- .../alert_forwarder/main/lambda.go | 5 +- .../alerts_api/api/get_alert_test.go | 67 +++++++- .../alerts_api/api/list_alerts_test.go | 12 ++ .../forwarder => alerts_api/models}/models.go | 24 ++- .../models}/models_test.go | 2 +- .../log_analysis/alerts_api/table/list.go | 121 ++++++++++---- .../log_analysis/alerts_api/table/table.go | 8 + .../log_analysis/alerts_api/utils/utils.go | 21 ++- web/__generated__/schema.tsx | 149 +++++++++++++++--- web/__tests__/__mocks__/builders.generated.ts | 60 +++++-- .../components/Editor/__mocks__/Editor.tsx | 9 -- web/src/components/Editor/__mocks__/index.tsx | 9 -- .../cards/AlertCard/AlertCard.test.tsx | 54 ++++--- .../components/cards/AlertCard/AlertCard.tsx | 15 +- .../__snapshots__/AlertCard.test.tsx.snap | 20 +-- .../useListAvailableDestinations.tsx | 9 -- .../fragments/AlertDetailsFull.generated.ts | 46 ++++-- .../fragments/AlertDetailsFull.graphql | 22 ++- .../fragments/AlertSummaryFull.generated.ts | 29 +++- .../fragments/AlertSummaryFull.graphql | 16 +- .../fragments/PolicyBasic.generated.ts | 57 +++++++ web/src/graphql/fragments/PolicyBasic.graphql | 15 ++ .../pages/AlertDetails/AlertDetails.test.tsx | 22 ++- web/src/pages/AlertDetails/AlertDetails.tsx | 16 +- .../AlertDetailsBanner/AlertDetailsBanner.tsx | 4 +- .../AlertDetailsBanner.test.tsx.snap | 6 +- .../AlertDetailsEvents/AlertDetailsEvents.tsx | 13 +- .../AlertDetailsInfo/AlertDetailsInfo.tsx | 6 +- .../graphql/policyTeaser.generated.ts | 93 +++++++++++ .../AlertDetails/graphql/policyTeaser.graphql | 5 + web/src/pages/AlertDetails/index.tsx | 1 + web/src/pages/ListAlerts/ListAlerts.test.tsx | 23 ++- web/src/pages/ListAlerts/ListAlerts.tsx | 5 +- .../LogAnalysisOverview.test.tsx | 32 +++- .../LogSourceCard/LogSourceCard.tsx | 9 -- .../LogSourceOnboarding.tsx | 9 -- .../RuleAlertsListing/RuleAlertsListing.tsx | 12 +- .../pages/RuleDetails/RuleDetails.test.tsx | 53 ++++--- web/src/pages/RuleDetails/RuleDetails.tsx | 6 +- 58 files changed, 1389 insertions(+), 448 deletions(-) create mode 100644 internal/compliance/alert_forwarder/forwarder/metrics.go create mode 100644 internal/compliance/alert_forwarder/main/config.go rename internal/log_analysis/{alert_forwarder/forwarder => alerts_api/models}/models.go (89%) rename internal/log_analysis/{alert_forwarder/forwarder => alerts_api/models}/models_test.go (99%) create mode 100644 web/src/graphql/fragments/PolicyBasic.generated.ts create mode 100644 web/src/graphql/fragments/PolicyBasic.graphql create mode 100644 web/src/pages/AlertDetails/graphql/policyTeaser.generated.ts create mode 100644 web/src/pages/AlertDetails/graphql/policyTeaser.graphql diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 8ed3ff0953..c491518686 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -99,12 +99,11 @@ input ListAlertsInput { exclusiveStartKey: String severity: [SeverityEnum] logTypes: [String!] - type: AlertTypesEnum + resourceTypes: [String!] + types: [AlertTypesEnum!] nameContains: String createdAtBefore: AWSDateTime createdAtAfter: AWSDateTime - ruleIdContains: String - alertIdContains: String status: [AlertStatusesEnum] eventCountMin: Int eventCountMax: Int @@ -145,16 +144,39 @@ type SqsLogIntegrationHealth { sqsStatus: IntegrationItemHealthStatus } +type AlertSummaryPolicyInfo { + policyId: ID + resourceId: String + policySourceId: String! + resourceTypes: [String!]! +} + +type AlertSummaryRuleInfo { + ruleId: ID + logTypes: [String!]! + eventsMatched: Int! +} + +type AlertDetailsRuleInfo { + ruleId: ID + logTypes: [String!]! + eventsMatched: Int! + dedupString: String! + events: [AWSJSON!]! + eventsLastEvaluatedKey: String +} + +union AlertSummaryDetectionInfo = AlertSummaryRuleInfo | AlertSummaryPolicyInfo +union AlertDetailsDetectionInfo = AlertDetailsRuleInfo | AlertSummaryPolicyInfo + interface Alert { alertId: ID! creationTime: AWSDateTime! deliveryResponses: [DeliveryResponse]! - eventsMatched: Int! - ruleId: ID severity: SeverityEnum! status: AlertStatusesEnum! title: String! - logTypes: [String!]! + type: AlertTypesEnum! lastUpdatedBy: ID # gets mapped to a User in the frontend lastUpdatedByTime: AWSDateTime # stores the timestamp of the last person who modified the Alert updateTime: AWSDateTime! # stores the timestamp from an update from a dedup event @@ -164,35 +186,28 @@ type AlertDetails implements Alert { alertId: ID! creationTime: AWSDateTime! deliveryResponses: [DeliveryResponse]! - eventsMatched: Int! - ruleId: ID severity: SeverityEnum! status: AlertStatusesEnum! title: String! type: AlertTypesEnum! - logTypes: [String!]! lastUpdatedBy: ID # gets mapped to a User in the frontend lastUpdatedByTime: AWSDateTime # stores the timestamp of the last person who modified the Alert updateTime: AWSDateTime! # stores the timestamp from an update from a dedup event - dedupString: String! - events: [AWSJSON!]! - eventsLastEvaluatedKey: String + detection: AlertDetailsDetectionInfo! } type AlertSummary implements Alert { alertId: ID! creationTime: AWSDateTime! deliveryResponses: [DeliveryResponse]! - eventsMatched: Int! - ruleId: ID type: AlertTypesEnum! severity: SeverityEnum! status: AlertStatusesEnum! title: String! - logTypes: [String!]! lastUpdatedBy: ID # gets mapped to a User in the frontend lastUpdatedByTime: AWSDateTime # stores the timestamp of the last person who modified the Alert updateTime: AWSDateTime! # stores the timestamp from an update from a dedup event + detection: AlertSummaryDetectionInfo! } type DeliveryResponse { @@ -1079,6 +1094,7 @@ enum AlertStatusesEnum { enum AlertTypesEnum { RULE RULE_ERROR + POLICY } enum SortDirEnum { diff --git a/api/lambda/alerts/models/api.go b/api/lambda/alerts/models/api.go index 6af2f844c5..395105e251 100644 --- a/api/lambda/alerts/models/api.go +++ b/api/lambda/alerts/models/api.go @@ -65,8 +65,6 @@ type GetAlertOutput = Alert // "nameContains": "string in alert title", // "createdAtAfter": "2020-06-17T15:49:40Z", // "createdAtBefore": "2020-06-17T15:49:40Z", -// "ruleIdContains": "string in rule id", -// "alertIdContains": "string in alert id", // "eventCountMin": "0", // "eventCountMax": "500", // "sortDir": "ascending", @@ -83,17 +81,16 @@ type ListAlertsInput struct { ExclusiveStartKey *string `json:"exclusiveStartKey"` // Filtering - Type string `json:"type" validate:"omitempty,oneof=RULE RULE_ERROR"` + Types []string `json:"types" validate:"omitempty,dive,oneof=RULE RULE_ERROR POLICY"` Severity []string `json:"severity" validate:"omitempty,dive,oneof=INFO LOW MEDIUM HIGH CRITICAL"` NameContains *string `json:"nameContains"` Status []string `json:"status" validate:"omitempty,dive,oneof=OPEN TRIAGED CLOSED RESOLVED"` CreatedAtBefore *time.Time `json:"createdAtBefore"` CreatedAtAfter *time.Time `json:"createdAtAfter"` - RuleIDContains *string `json:"ruleIdContains"` - AlertIDContains *string `json:"alertIdContains"` EventCountMin *int `json:"eventCountMin" validate:"omitempty,min=0"` EventCountMax *int `json:"eventCountMax" validate:"omitempty,min=1"` LogTypes []string `json:"logTypes" validate:"omitempty,dive,required"` + ResourceTypes []string `json:"resourceTypes" validate:"omitempty,dive,required"` // Sorting SortDir *string `json:"sortDir" validate:"omitempty,oneof=ascending descending"` } @@ -184,22 +181,28 @@ type ListAlertsOutput struct { // AlertSummary contains summary information for an alert type AlertSummary struct { - AlertID string `json:"alertId" validate:"required"` - Type string `json:"type,omitempty"` - RuleID *string `json:"ruleId" validate:"required"` - RuleDisplayName *string `json:"ruleDisplayName,omitempty"` - RuleVersion *string `json:"ruleVersion" validate:"required"` - DedupString *string `json:"dedupString,omitempty"` - DeliveryResponses []*DeliveryResponse `json:"deliveryResponses" validate:"required"` - LogTypes []string `json:"logTypes" validate:"required"` - CreationTime *time.Time `json:"creationTime" validate:"required"` - UpdateTime *time.Time `json:"updateTime" validate:"required"` - EventsMatched *int `json:"eventsMatched" validate:"required"` - Severity *string `json:"severity" validate:"required"` + AlertID string `json:"alertId"` + Type string `json:"type"` + RuleID *string `json:"ruleId"` + RuleDisplayName *string `json:"ruleDisplayName"` + RuleVersion *string `json:"ruleVersion"` + DedupString *string `json:"dedupString"` + DeliveryResponses []*DeliveryResponse `json:"deliveryResponses"` + LogTypes []string `json:"logTypes"` + CreationTime *time.Time `json:"creationTime"` + UpdateTime *time.Time `json:"updateTime"` + EventsMatched *int `json:"eventsMatched"` + Severity *string `json:"severity"` Status string `json:"status,omitempty"` - Title *string `json:"title" validate:"required"` - LastUpdatedBy string `json:"lastUpdatedBy,omitempty"` - LastUpdatedByTime time.Time `json:"lastUpdatedByTime,omitempty"` + Title *string `json:"title"` + LastUpdatedBy string `json:"lastUpdatedBy"` + LastUpdatedByTime time.Time `json:"lastUpdatedByTime"` + PolicyID string `json:"policyId"` + PolicyDisplayName string `json:"policyDisplayName"` + PolicySourceID string `json:"policySourceId"` + PolicyVersion string `json:"policyVersion"` + ResourceTypes []string `json:"resourceTypes"` + ResourceID string `json:"resourceId"` } // Alert contains the details of an alert diff --git a/api/lambda/delivery/models/api.go b/api/lambda/delivery/models/api.go index 7ef967e013..f4db6493fd 100644 --- a/api/lambda/delivery/models/api.go +++ b/api/lambda/delivery/models/api.go @@ -122,7 +122,7 @@ type DeliverAlertOutput = alertModels.AlertSummary // Alert is the schema for each row in the Dynamo alerts table. type Alert struct { - // ID is the rule that triggered the alert. + // ID is the rule/policy that triggered the alert. AnalysisID string `json:"analysisId" validate:"required"` // Type specifies if an alert is for a policy or a rule @@ -140,7 +140,16 @@ type Alert struct { // LogTypes is the set of logs that could trigger the alert. LogTypes []string `json:"logTypes,omitempty"` - // AnalysisDescription is the description of the rule that triggered the alert. + // ResourceTypes is the set of resources that could trigger the alert. + ResourceTypes []string `json:"resourceTypes,omitempty"` + + // ResourceID is the ID of the failing resource in the policy. + ResourceID string `json:"resourceId,omitempty"` + + // AnalysisSourceID is the ID of the source integration for the rule/policy that failed. + AnalysisSourceID string `json:"analysisSourceId,omitempty"` + + // AnalysisDescription is the description of the rule/policy that triggered the alert. AnalysisDescription string `json:"analysisDescription,omitempty"` // Name is the name of the policy at the time the alert was triggered. diff --git a/deployments/appsync.yml b/deployments/appsync.yml index ef90feef8b..747c60a5ea 100644 --- a/deployments/appsync.yml +++ b/deployments/appsync.yml @@ -1382,7 +1382,37 @@ Resources: "listAlerts": $ctx.args.input }) } - ResponseMappingTemplate: !FindInMap [ResponseTemplates, Lambda, VTL] + ResponseMappingTemplate: | + #if($ctx.error) + $util.error($ctx.error.errorMessage, $ctx.error.errorType, $ctx.args) + #else + #set($alerts = []) + + #foreach($item in $ctx.result.alertSummaries) + #set($alert = $item) + #if ($item.type == "POLICY") + $util.qr($alert.put("detection", { + "__typename": "AlertSummaryPolicyInfo", + "policyId": $item.policyId, + "policySourceId": $item.policySourceId, + "resourceId": $item.resourceId, + "resourceTypes": $item.resourceTypes + })) + #else + $util.qr($alert.put("detection", { + "__typename": "AlertSummaryRuleInfo", + "ruleId": $item.ruleId, + "logTypes": $item.logTypes, + "eventsMatched": $item.eventsMatched + })) + #end + $util.qr($alerts.add($alert)) + #end + $util.toJson({ + "alertSummaries": $alerts, + "lastEvaluatedKey": $ctx.result.lastEvaluatedKey + }) + #end GetAlertResolver: Type: AWS::AppSync::Resolver @@ -1399,7 +1429,32 @@ Resources: "getAlert": $ctx.args.input }) } - ResponseMappingTemplate: !FindInMap [ResponseTemplates, Lambda, VTL] + ResponseMappingTemplate: | + #if($ctx.error) + $util.error($ctx.error.errorMessage, $ctx.error.errorType, $ctx.args) + #else + #set($payload = $ctx.result) + #if ($payload.type == "POLICY") + $util.qr($payload.put("detection", { + "__typename": "AlertSummaryPolicyInfo", + "policyId": $ctx.result.policyId, + "policySourceId": $ctx.result.policySourceId, + "resourceId": $ctx.result.resourceId, + "resourceTypes": $ctx.result.resourceTypes + })) + #else + $util.qr($payload.put("detection", { + "__typename": "AlertDetailsRuleInfo", + "ruleId": $ctx.result.ruleId, + "logTypes": $ctx.result.logTypes, + "eventsMatched": $ctx.result.eventsMatched, + "dedupString": $ctx.result.dedupString, + "events": $ctx.result.events, + "eventsLastEvaluatedKey": $ctx.result.eventsLastEvaluatedKey + })) + #end + $util.toJson($payload) + #end UpdateAlertStatusResolver: Type: AWS::AppSync::Resolver @@ -1418,7 +1473,34 @@ Resources: "updateAlertStatus": $input }) } - ResponseMappingTemplate: !FindInMap [ResponseTemplates, Lambda, VTL] + ResponseMappingTemplate: | + #if($ctx.error) + $util.error($ctx.error.errorMessage, $ctx.error.errorType, $ctx.args) + #else + #set($alerts = []) + + #foreach($item in $ctx.result) + #set($alert = $item) + #if ($item.type == "POLICY") + $util.qr($alert.put("detection", { + "__typename": "AlertSummaryPolicyInfo", + "policyId": $item.policyId, + "policySourceId": $item.policySourceId, + "resourceId": $item.resourceId, + "resourceTypes": $item.resourceTypes + })) + #else + $util.qr($alert.put("detection", { + "__typename": "AlertSummaryRuleInfo", + "ruleId": $item.ruleId, + "logTypes": $item.logTypes, + "eventsMatched": $item.eventsMatched + })) + #end + $util.qr($alerts.add($alert)) + #end + $util.toJson($alerts) + #end TestPolicyResolver: Type: AWS::AppSync::Resolver diff --git a/deployments/cloud_security.yml b/deployments/cloud_security.yml index df1672eaf1..a863c819ba 100644 --- a/deployments/cloud_security.yml +++ b/deployments/cloud_security.yml @@ -294,8 +294,9 @@ Resources: Description: Forwards the alerts to the alert delivery mechanism Environment: Variables: - ALERTING_QUEUE_URL: !Sub https://sqs.${AWS::Region}.${AWS::URLSuffix}/${AWS::AccountId}/panther-alerts-queue DEBUG: !Ref Debug + ALERTS_TABLE: panther-log-alert-info + ALERTING_QUEUE_URL: !Sub https://sqs.${AWS::Region}.${AWS::URLSuffix}/${AWS::AccountId}/panther-alerts-queue Events: DynamoDBEvent: Type: DynamoDB @@ -305,12 +306,14 @@ Resources: BatchSize: 1 FunctionName: panther-alert-forwarder # - # The `panther-alert-forwarder` lambda reads from the ddb stream for the table `panther-alert-forwarder` - # and sends them to the `panther-alerts-queue` sqs queue. + # This lambda reads from a DDB stream for the `panther-alert-forwarder` table and writes alerts to the `panther-log-alert-info` ddb table. + # It also forwards alerts to `panther-alerts-queue` SQS queue where the appropriate Lambda picks them up for delivery. # # Failure Impact - # * Failure of this lambda will stop delivery of alerts to destinations. + # * Delivery of alerts could be slowed or stopped. # * There will be no data loss until events are purged from the ddb stream (24 hours). + # * This Lambda processes alerts in batches. In case a batch partially fails, the whole batch will be retried which might lead + # to duplicate notifications for some alerts. # Handler: main MemorySize: !FindInMap [Functions, AlertForwarder, Memory] @@ -318,6 +321,14 @@ Resources: Layers: !If [AttachLayers, !Ref LayerVersionArns, !Ref AWS::NoValue] Timeout: !FindInMap [Functions, AlertForwarder, Timeout] Policies: + - Id: ManageAlerts + Version: 2012-10-17 + Statement: + - Effect: Allow + Action: + - dynamodb:PutItem + - dynamodb:UpdateItem + Resource: !Sub arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/panther-log-alert-info - Id: PublishToAlertQueue Version: 2012-10-17 Statement: diff --git a/go.mod b/go.mod index 3a123d2ff4..5d8c8fb110 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( golang.org/x/text v0.3.4 // indirect golang.org/x/tools v0.0.0-20201110175055-ae6603bdc3c4 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 gopkg.in/go-playground/validator.v9 v9.31.0 gopkg.in/yaml.v2 v2.3.0 ) diff --git a/internal/compliance/alert_forwarder/forwarder/forwarder.go b/internal/compliance/alert_forwarder/forwarder/forwarder.go index af90ee2599..1b17f7e665 100644 --- a/internal/compliance/alert_forwarder/forwarder/forwarder.go +++ b/internal/compliance/alert_forwarder/forwarder/forwarder.go @@ -19,42 +19,118 @@ package forwarder */ import ( - "os" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" "github.com/aws/aws-sdk-go/service/sqs" "github.com/aws/aws-sdk-go/service/sqs/sqsiface" jsoniter "github.com/json-iterator/go" - "go.uber.org/zap" + "github.com/pkg/errors" "github.com/panther-labs/panther/api/lambda/delivery/models" + alertApiModels "github.com/panther-labs/panther/internal/log_analysis/alerts_api/models" + "github.com/panther-labs/panther/pkg/metrics" ) -var ( - alertQueueURL = os.Getenv("ALERTING_QUEUE_URL") - awsSession = session.Must(session.NewSession()) - sqsClient sqsiface.SQSAPI = sqs.New(awsSession) -) +const defaultTimePartition = "defaultPartition" -// Handle forwards an alert to the alert delivery SQS queue -func Handle(event *models.Alert) error { - zap.L().Info("received alert", zap.String("policyId", event.AnalysisID)) +type Handler struct { + SqsClient sqsiface.SQSAPI + DdbClient dynamodbiface.DynamoDBAPI + AlertTable string + AlertingQueueURL string + MetricsLogger metrics.Logger +} - msgBody, err := jsoniter.Marshal(event) - if err != nil { +func (h *Handler) Do(alert models.Alert) error { + // Persist to DDB + if err := h.storeNewAlert(alert); err != nil { + return errors.Wrap(err, "failed to store new alert (policy) in DDB") + } + + // Send to Dispatch queue + if err := h.sendAlertNotification(alert); err != nil { return err } - input := &sqs.SendMessageInput{ - QueueUrl: aws.String(alertQueueURL), - MessageBody: aws.String(string(msgBody)), + + // Log stats + if alert.Type == models.PolicyType { + h.logStats(alert) + } + + return nil +} + +func (h *Handler) logStats(alert models.Alert) { + h.MetricsLogger.Log( + []metrics.Dimension{ + {Name: "Severity", Value: alert.Severity}, + {Name: "AnalysisType", Value: "Policy"}, + {Name: "AnalysisID", Value: alert.AnalysisID}, + }, + metrics.Metric{ + Name: "AlertsCreated", + Value: 1, + Unit: metrics.UnitCount, + }, + ) +} + +func (h *Handler) storeNewAlert(alert models.Alert) error { + // Here we re-use the same field names for alerts that were + // generated from rules. + dynamoAlert := &alertApiModels.Alert{ + ID: *alert.AlertID, + TimePartition: defaultTimePartition, + Severity: aws.String(alert.Severity), + Title: alert.Title, + AlertPolicy: alertApiModels.AlertPolicy{ + PolicyID: alert.AnalysisID, + PolicyDisplayName: aws.StringValue(alert.AnalysisName), + PolicyVersion: aws.StringValue(alert.Version), + PolicySourceID: alert.AnalysisSourceID, + ResourceTypes: alert.ResourceTypes, + ResourceID: alert.ResourceID, + }, + // Reuse part of the struct that was intended for Rules + AlertDedupEvent: alertApiModels.AlertDedupEvent{ + RuleID: alert.AnalysisID, // Not used, but needed to meet the `ruleId-creationTime-index` constraint + CreationTime: alert.CreatedAt, + UpdateTime: alert.CreatedAt, + Type: alert.Type, + }, } - _, err = sqsClient.SendMessage(input) + + marshaledAlert, err := dynamodbattribute.MarshalMap(dynamoAlert) if err != nil { - zap.L().Warn("failed to send message to remediation", zap.Error(err)) - return err + return errors.Wrap(err, "failed to marshal alert") + } + putItemRequest := &dynamodb.PutItemInput{ + Item: marshaledAlert, + TableName: &h.AlertTable, + } + _, err = h.DdbClient.PutItem(putItemRequest) + if err != nil { + return errors.Wrap(err, "failed to store alert") } - zap.L().Info("successfully triggered alert action") return nil } + +func (h *Handler) sendAlertNotification(alert models.Alert) error { + msgBody, err := jsoniter.MarshalToString(alert) + if err != nil { + return errors.Wrap(err, "failed to marshal alert notification") + } + + input := &sqs.SendMessageInput{ + QueueUrl: &h.AlertingQueueURL, + MessageBody: &msgBody, + } + _, err = h.SqsClient.SendMessage(input) + if err != nil { + return errors.Wrap(err, "failed to send notification") + } + return nil +} diff --git a/internal/compliance/alert_forwarder/forwarder/forwarder_test.go b/internal/compliance/alert_forwarder/forwarder/forwarder_test.go index f530f619db..9dc4f70b88 100644 --- a/internal/compliance/alert_forwarder/forwarder/forwarder_test.go +++ b/internal/compliance/alert_forwarder/forwarder/forwarder_test.go @@ -19,52 +19,110 @@ package forwarder */ import ( - "errors" "testing" + "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" "github.com/aws/aws-sdk-go/service/sqs" jsoniter "github.com/json-iterator/go" - "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/panther-labs/panther/api/lambda/delivery/models" + alertApiModels "github.com/panther-labs/panther/internal/log_analysis/alerts_api/models" + "github.com/panther-labs/panther/pkg/metrics" "github.com/panther-labs/panther/pkg/testutils" ) -func init() { - alertQueueURL = "alertQueueURL" +var ( + expectedMetric = []metrics.Metric{{Name: "AlertsCreated", Value: 1, Unit: metrics.UnitCount}} + expectedDimensions = []metrics.Dimension{ + {Name: "Severity", Value: "INFO"}, + {Name: "AnalysisType", Value: "Policy"}, + {Name: "AnalysisID", Value: "Test.Policy"}} + timeNow = time.Unix(1581379785, 0).UTC() // Set a static time +) + +func genSampleAlert() models.Alert { + return models.Alert{ + AlertID: aws.String("26df596024d2e81140de028387d517da"), // This is generated dynamically + CreatedAt: timeNow, + Severity: "INFO", + Title: "some title", + AnalysisID: "Test.Policy", + AnalysisName: aws.String("A test policy to generate alerts"), + AnalysisDescription: "An alert triggered from a Policy...", + AnalysisSourceID: "9d1f16f0-8bcc-11ea-afeb-efa9a81fb878", + Version: aws.String("A policy version"), + ResourceTypes: []string{"Resource", "Types"}, + ResourceID: "arn:aws:iam::xxx...", + Runbook: "Check out our docs!", + Tags: []string{"Tag", "Policy", "AWS"}, + Type: models.PolicyType, + } } -func TestHandleAlert(t *testing.T) { - mockSqsClient := &testutils.SqsMock{} - sqsClient = mockSqsClient +func TestHandleStoreAndSendNotification(t *testing.T) { + t.Parallel() + ddbMock := &testutils.DynamoDBMock{} + sqsMock := &testutils.SqsMock{} + metricsMock := &testutils.LoggerMock{} - input := &models.Alert{ - AnalysisID: "policyId", + handler := &Handler{ + AlertTable: "alertsTable", + AlertingQueueURL: "queueUrl", + DdbClient: ddbMock, + SqsClient: sqsMock, + MetricsLogger: metricsMock, } - expectedMsgBody, err := jsoniter.MarshalToString(input) + expectedAlert := genSampleAlert() + + // Next, simulate sending to SQS + expectedMarshaledAlert, err := jsoniter.MarshalToString(expectedAlert) require.NoError(t, err) - expectedInput := &sqs.SendMessageInput{ - QueueUrl: aws.String("alertQueueURL"), - MessageBody: aws.String(expectedMsgBody), + expectedSendMessageInput := &sqs.SendMessageInput{ + MessageBody: &expectedMarshaledAlert, + QueueUrl: aws.String("queueUrl"), } + sqsMock.On("SendMessage", expectedSendMessageInput).Return(&sqs.SendMessageOutput{}, nil) - mockSqsClient.On("SendMessage", expectedInput).Return(&sqs.SendMessageOutput{}, nil) - require.NoError(t, Handle(input)) - mockSqsClient.AssertExpectations(t) -} - -func TestHandleAlertSqsError(t *testing.T) { - mockSqsClient := &testutils.SqsMock{} - sqsClient = mockSqsClient - - input := &models.Alert{ - AnalysisID: "policyId", + // Then, simulate sending to DDB + expectedDynamoAlert := &alertApiModels.Alert{ + ID: "26df596024d2e81140de028387d517da", + TimePartition: "defaultPartition", + Severity: aws.String("INFO"), + Title: expectedAlert.Title, + AlertPolicy: alertApiModels.AlertPolicy{ + PolicyID: expectedAlert.AnalysisID, + PolicyDisplayName: aws.StringValue(expectedAlert.AnalysisName), + PolicyVersion: aws.StringValue(expectedAlert.Version), + PolicySourceID: expectedAlert.AnalysisSourceID, + ResourceTypes: expectedAlert.ResourceTypes, + ResourceID: expectedAlert.ResourceID, + }, + // Reuse part of the struct that was intended for Rules + AlertDedupEvent: alertApiModels.AlertDedupEvent{ + RuleID: expectedAlert.AnalysisID, // Required for DDB GSI constraint + CreationTime: expectedAlert.CreatedAt, + UpdateTime: expectedAlert.CreatedAt, + Type: expectedAlert.Type, + }, } + expectedMarshaledDynamoAlert, err := dynamodbattribute.MarshalMap(expectedDynamoAlert) + assert.NoError(t, err) + expectedPutItemRequest := &dynamodb.PutItemInput{ + Item: expectedMarshaledDynamoAlert, + TableName: aws.String("alertsTable"), + } + + ddbMock.On("PutItem", expectedPutItemRequest).Return(&dynamodb.PutItemOutput{}, nil) + metricsMock.On("Log", expectedDimensions, expectedMetric).Once() + assert.NoError(t, handler.Do(expectedAlert)) - mockSqsClient.On("SendMessage", mock.Anything).Return(&sqs.SendMessageOutput{}, errors.New("error")) - require.Error(t, Handle(input)) - mockSqsClient.AssertExpectations(t) + ddbMock.AssertExpectations(t) + sqsMock.AssertExpectations(t) + metricsMock.AssertExpectations(t) } diff --git a/internal/compliance/alert_forwarder/forwarder/metrics.go b/internal/compliance/alert_forwarder/forwarder/metrics.go new file mode 100644 index 0000000000..60fc62c265 --- /dev/null +++ b/internal/compliance/alert_forwarder/forwarder/metrics.go @@ -0,0 +1,48 @@ +package forwarder + +/** + * Panther is a Cloud-Native SIEM for the Modern Security Team. + * Copyright (C) 2020 Panther Labs Inc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import ( + "github.com/panther-labs/panther/pkg/metrics" +) + +var ( + StaticLogger = metrics.MustStaticLogger([]metrics.DimensionSet{ + { + "AnalysisType", + "Severity", + }, + { + "AnalysisType", + "AnalysisID", + }, + { + "AnalysisType", + }, + }, []metrics.Metric{ + { + Name: "AlertsCreated", + Unit: metrics.UnitCount, + }, + }) + AnalysisTypeDimension = metrics.Dimension{ + Name: "AnalysisType", + Value: "Policy", + } +) diff --git a/internal/compliance/alert_forwarder/main/config.go b/internal/compliance/alert_forwarder/main/config.go new file mode 100644 index 0000000000..aea003b30c --- /dev/null +++ b/internal/compliance/alert_forwarder/main/config.go @@ -0,0 +1,49 @@ +package main + +/** + * Panther is a Cloud-Native SIEM for the Modern Security Team. + * Copyright (C) 2020 Panther Labs Inc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import ( + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" + "github.com/aws/aws-sdk-go/service/sqs" + "github.com/aws/aws-sdk-go/service/sqs/sqsiface" + "github.com/kelseyhightower/envconfig" +) + +var ( + env envConfig + awsSession *session.Session + ddbClient dynamodbiface.DynamoDBAPI + sqsClient sqsiface.SQSAPI +) + +type envConfig struct { + AlertsTable string `required:"true" split_words:"true"` + AlertingQueueURL string `required:"true" split_words:"true"` +} + +// Setup parses the environment and builds the AWS and http clients. +func Setup() { + envconfig.MustProcess("", &env) + + awsSession = session.Must(session.NewSession()) + ddbClient = dynamodb.New(awsSession) + sqsClient = sqs.New(awsSession) +} diff --git a/internal/compliance/alert_forwarder/main/lambda.go b/internal/compliance/alert_forwarder/main/lambda.go index 68c1b70b22..2b7a4f291c 100644 --- a/internal/compliance/alert_forwarder/main/lambda.go +++ b/internal/compliance/alert_forwarder/main/lambda.go @@ -32,30 +32,58 @@ import ( "github.com/panther-labs/panther/api/lambda/delivery/models" "github.com/panther-labs/panther/internal/compliance/alert_forwarder/forwarder" "github.com/panther-labs/panther/pkg/lambdalogger" + "github.com/panther-labs/panther/pkg/metrics" "github.com/panther-labs/panther/pkg/oplog" ) const alertConfigKey = "alertConfig" -var validate = validator.New() +var ( + validate = validator.New() + handler *forwarder.Handler +) func main() { - lambda.Start(reporterHandler) + // Required only once per Lambda container + Setup() + // TODO: revisit this. Not sure why we neeed Dimension sets and why just an array of dimensions is not enough + metricsLogger := metrics.MustLogger([]metrics.DimensionSet{ + { + "AnalysisType", + "Severity", + }, + { + "AnalysisType", + }, + }) + handler = &forwarder.Handler{ + SqsClient: sqsClient, + DdbClient: ddbClient, + AlertingQueueURL: env.AlertingQueueURL, + AlertTable: env.AlertsTable, + MetricsLogger: metricsLogger, + } + lambda.Start(handle) } -func reporterHandler(ctx context.Context, event events.DynamoDBEvent) (err error) { +func handle(ctx context.Context, event events.DynamoDBEvent) error { lc, _ := lambdalogger.ConfigureGlobal(ctx, nil) + return reporterHandler(lc, event) +} + +func reporterHandler(lc *lambdacontext.LambdaContext, event events.DynamoDBEvent) (err error) { operation := oplog.NewManager("cloudsec", "alert_forwarder").Start(lc.InvokedFunctionArn).WithMemUsed(lambdacontext.MemoryLimitInMB) defer func() { - operation.Stop().Log(err, zap.Int("numEvents", len(event.Records))) + operation.Stop().Log(err, zap.Int("messageCount", len(event.Records))) }() for _, record := range event.Records { + // Skip events that aren't new if record.Change.NewImage == nil { zap.L().Debug("Skipping record", zap.Any("record", record)) continue } - var alert models.Alert + alert := models.Alert{} if err = jsoniter.Unmarshal(record.Change.NewImage[alertConfigKey].Binary(), &alert); err != nil { operation.LogError(errors.Wrap(err, "Failed to unmarshal item")) continue @@ -66,8 +94,8 @@ func reporterHandler(ctx context.Context, event events.DynamoDBEvent) (err error continue } - if err = forwarder.Handle(&alert); err != nil { - err = errors.Wrap(err, "encountered issue while processing event") + if err = handler.Do(alert); err != nil { + err = errors.Wrap(err, "encountered issue while handling policy event") return err } } diff --git a/internal/compliance/alert_processor/models/api.go b/internal/compliance/alert_processor/models/api.go index 1904614ae2..8aa07f10cb 100644 --- a/internal/compliance/alert_processor/models/api.go +++ b/internal/compliance/alert_processor/models/api.go @@ -34,6 +34,9 @@ type ComplianceNotification struct { // PolicyVersionID is the version of policy when the alert triggered PolicyVersionID string `json:"policyVersionId"` + // PolicySourceID is the id of the source integration + PolicySourceID string `json:"policySourceId" validate:"required,min=1"` + // ResourceID is the ID specific to the resource ResourceID string `json:"resourceId" validate:"required,min=1"` diff --git a/internal/compliance/alert_processor/processor/processor.go b/internal/compliance/alert_processor/processor/processor.go index 1c81ac4a2d..e7ea87f239 100644 --- a/internal/compliance/alert_processor/processor/processor.go +++ b/internal/compliance/alert_processor/processor/processor.go @@ -19,6 +19,8 @@ package processor */ import ( + "crypto/md5" // nolint: gosec + "encoding/hex" "net/http" "os" "time" @@ -211,9 +213,13 @@ func getAlertConfigPolicy(event *models.ComplianceNotification) (*alertmodel.Ale } return &alertmodel.Alert{ + AlertID: GenerateAlertID(event), AnalysisDescription: policy.Description, AnalysisID: event.PolicyID, AnalysisName: &policy.DisplayName, + ResourceTypes: policy.ResourceTypes, + ResourceID: event.ResourceID, + AnalysisSourceID: event.PolicySourceID, CreatedAt: event.Timestamp, OutputIds: event.OutputIds, Runbook: policy.Runbook, @@ -225,3 +231,11 @@ func getAlertConfigPolicy(event *models.ComplianceNotification) (*alertmodel.Ale policy.AutoRemediationID != "", // means we can remediate nil } + +// generates an ID from the policyID (policy name) and the current timestamp. +func GenerateAlertID(event *models.ComplianceNotification) *string { + key := event.PolicyID + ":" + event.Timestamp.String() + keyHash := md5.Sum([]byte(key)) // nolint(gosec) + encoded := hex.EncodeToString(keyHash[:]) + return &encoded +} diff --git a/internal/compliance/alert_processor/processor/processor_test.go b/internal/compliance/alert_processor/processor/processor_test.go index 403606eeb2..c34be3ec3b 100644 --- a/internal/compliance/alert_processor/processor/processor_test.go +++ b/internal/compliance/alert_processor/processor/processor_test.go @@ -27,6 +27,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "gopkg.in/go-playground/assert.v1" analysismodels "github.com/panther-labs/panther/api/lambda/analysis/models" compliancemodels "github.com/panther-labs/panther/api/lambda/compliance/models" @@ -36,6 +37,18 @@ import ( "github.com/panther-labs/panther/pkg/testutils" ) +var timeNow = time.Unix(1581379785, 0).UTC() // Set a static time + +func genSampleEvent() *models.ComplianceNotification { + return &models.ComplianceNotification{ + ResourceID: "arn:aws:iam::xxx...", + PolicyID: "Test.Policy", + PolicyVersionID: "A policy version", + ShouldAlert: true, + Timestamp: timeNow, + } +} + func TestHandleEventWithAlert(t *testing.T) { mockDdbClient := &testutils.DynamoDBMock{} ddbClient = mockDdbClient @@ -52,7 +65,7 @@ func TestHandleEventWithAlert(t *testing.T) { PolicyID: "test-policy", PolicyVersionID: "test-version", ShouldAlert: true, - Timestamp: time.Now(), + Timestamp: time.Now().UTC(), } complianceResponse := &compliancemodels.ComplianceEntry{ @@ -115,11 +128,11 @@ func TestHandleEventWithAlertButNoAutoRemediationID(t *testing.T) { PolicyID: "test-policy", PolicyVersionID: "test-version", ShouldAlert: true, - Timestamp: time.Now(), + Timestamp: time.Now().UTC(), } complianceResponse := &compliancemodels.ComplianceEntry{ - LastUpdated: time.Now(), + LastUpdated: time.Now().UTC(), PolicyID: "test-policy", PolicySeverity: "INFO", ResourceID: "test-resource", @@ -168,7 +181,7 @@ func TestHandleEventWithoutAlert(t *testing.T) { } complianceResponse := &compliancemodels.ComplianceEntry{ - LastUpdated: time.Now(), + LastUpdated: time.Now().UTC(), PolicyID: "test-policy", PolicySeverity: "INFO", ResourceID: "test-resource", @@ -203,7 +216,7 @@ func TestSkipActionsIfResourceIsNotFailing(t *testing.T) { } responseBody := &compliancemodels.ComplianceEntry{ - LastUpdated: time.Now(), + LastUpdated: time.Now().UTC(), PolicyID: "test-policy", PolicySeverity: "INFO", ResourceID: "test-resource", @@ -247,3 +260,9 @@ func TestSkipActionsIfLookupFailed(t *testing.T) { mockComplianceClient.AssertExpectations(t) mockDdbClient.AssertExpectations(t) } + +func TestGenerateAlertID(t *testing.T) { + event := genSampleEvent() + eventID := GenerateAlertID(event) + assert.Equal(t, *eventID, "26df596024d2e81140de028387d517da") +} diff --git a/internal/compliance/resource_processor/processor/handler.go b/internal/compliance/resource_processor/processor/handler.go index ce01f47142..cc575f5fd0 100644 --- a/internal/compliance/resource_processor/processor/handler.go +++ b/internal/compliance/resource_processor/processor/handler.go @@ -232,12 +232,12 @@ func (r *batchResults) analyze(resources resourceMap, policies policyMap) error // Every failed policy, if not suppressed, will trigger the remediation flow complianceNotification := &alertmodels.ComplianceNotification{ - OutputIds: policy.OutputIDs, - ResourceID: resource.ID, PolicyID: policy.ID, + PolicySourceID: resource.IntegrationID, PolicyVersionID: policy.VersionID, - Timestamp: time.Now(), - + ResourceID: resource.ID, + OutputIds: policy.OutputIDs, + Timestamp: time.Now().UTC(), // We only need to send an alert to the user if the status is newly FAILing ShouldAlert: status != compliancemodels.StatusFail, } diff --git a/internal/core/alert_delivery/api/update_delivery.go b/internal/core/alert_delivery/api/update_delivery.go index 1c9b1aa54e..0be929ed18 100644 --- a/internal/core/alert_delivery/api/update_delivery.go +++ b/internal/core/alert_delivery/api/update_delivery.go @@ -22,7 +22,6 @@ import ( "go.uber.org/zap" alertModels "github.com/panther-labs/panther/api/lambda/alerts/models" - deliveryModels "github.com/panther-labs/panther/api/lambda/delivery/models" "github.com/panther-labs/panther/pkg/genericapi" ) @@ -31,11 +30,6 @@ func updateAlerts(statuses []DispatchStatus) []*alertModels.AlertSummary { // create a relational mapping for alertID to a list of delivery statuses alertMap := make(map[string][]*alertModels.DeliveryResponse) for _, status := range statuses { - // If the alert came from a policy, we need to skip - if (status.Alert.Type == deliveryModels.PolicyType) || (status.Alert.AlertID == nil) { - continue - } - // convert to the response type the lambda expects deliveryResponse := &alertModels.DeliveryResponse{ OutputID: status.OutputID, diff --git a/internal/core/alert_delivery/api/update_delivery_test.go b/internal/core/alert_delivery/api/update_delivery_test.go index f545704719..ae44985af5 100644 --- a/internal/core/alert_delivery/api/update_delivery_test.go +++ b/internal/core/alert_delivery/api/update_delivery_test.go @@ -22,7 +22,6 @@ import ( "testing" "time" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/lambda" jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" @@ -61,7 +60,7 @@ func TestUpdateAlerts(t *testing.T) { { Alert: deliveryModels.Alert{ AlertID: &alertID, - Type: deliveryModels.RuleType, + Type: deliveryModels.PolicyType, OutputIds: outputIds, Severity: "INFO", CreatedAt: time.Now().UTC(), @@ -135,38 +134,6 @@ func TestUpdateAlerts(t *testing.T) { mockClient.AssertExpectations(t) } -func TestUpdateAlertSkipPolicy(t *testing.T) { - mockClient := &testutils.LambdaMock{} - lambdaClient = mockClient - - alertID := aws.String("alert-id") - outputIds := []string{"output-id-1"} - dispatchedAt := time.Now().UTC() - statuses := []DispatchStatus{ - { - Alert: deliveryModels.Alert{ - AlertID: alertID, - Type: deliveryModels.PolicyType, - OutputIds: outputIds, - Severity: "INFO", - CreatedAt: time.Now().UTC(), - }, - OutputID: outputIds[0], - Message: "success", - StatusCode: 200, - Success: true, - NeedsRetry: false, - DispatchedAt: dispatchedAt, - }, - } - - expectedResponse := []*alertModels.AlertSummary{} - - response := updateAlerts(statuses) - assert.Equal(t, expectedResponse, response) - mockClient.AssertExpectations(t) -} - func TestUpdateAlert(t *testing.T) { mockClient := &testutils.LambdaMock{} lambdaClient = mockClient diff --git a/internal/log_analysis/alert_forwarder/forwarder/forwarder.go b/internal/log_analysis/alert_forwarder/forwarder/forwarder.go index 5807dc7e2c..baa44ad7a1 100644 --- a/internal/log_analysis/alert_forwarder/forwarder/forwarder.go +++ b/internal/log_analysis/alert_forwarder/forwarder/forwarder.go @@ -36,6 +36,7 @@ import ( ruleModel "github.com/panther-labs/panther/api/lambda/analysis/models" alertModel "github.com/panther-labs/panther/api/lambda/delivery/models" + alertApiModels "github.com/panther-labs/panther/internal/log_analysis/alerts_api/models" "github.com/panther-labs/panther/pkg/metrics" ) @@ -52,7 +53,7 @@ type Handler struct { MetricsLogger metrics.Logger } -func (h *Handler) Do(oldAlertDedupEvent, newAlertDedupEvent *AlertDedupEvent) (err error) { +func (h *Handler) Do(oldAlertDedupEvent, newAlertDedupEvent *alertApiModels.AlertDedupEvent) (err error) { var oldRule *ruleModel.Rule if oldAlertDedupEvent != nil { oldRule, err = h.Cache.Get(oldAlertDedupEvent.RuleID, oldAlertDedupEvent.RuleVersion) @@ -75,12 +76,12 @@ func (h *Handler) Do(oldAlertDedupEvent, newAlertDedupEvent *AlertDedupEvent) (e return h.updateExistingAlert(newAlertDedupEvent) } -func shouldIgnoreChange(rule *ruleModel.Rule, alertDedupEvent *AlertDedupEvent) bool { +func shouldIgnoreChange(rule *ruleModel.Rule, alertDedupEvent *alertApiModels.AlertDedupEvent) bool { // If the number of matched events hasn't crossed the threshold for the rule, don't create a new alert. return alertDedupEvent.Type == alertModel.RuleType && alertDedupEvent.EventCount < int64(rule.Threshold) } -func needToCreateNewAlert(oldRule *ruleModel.Rule, oldAlertDedupEvent, newAlertDedupEvent *AlertDedupEvent) bool { +func needToCreateNewAlert(oldRule *ruleModel.Rule, oldAlertDedupEvent, newAlertDedupEvent *alertApiModels.AlertDedupEvent) bool { if oldAlertDedupEvent == nil { // If this is the first time we see an alert deduplication entry, create an alert return true @@ -96,7 +97,7 @@ func needToCreateNewAlert(oldRule *ruleModel.Rule, oldAlertDedupEvent, newAlertD return false } -func (h *Handler) handleNewAlert(rule *ruleModel.Rule, event *AlertDedupEvent) error { +func (h *Handler) handleNewAlert(rule *ruleModel.Rule, event *alertApiModels.AlertDedupEvent) error { if err := h.storeNewAlert(rule, event); err != nil { return errors.Wrap(err, "failed to store new alert in DDB") } @@ -108,7 +109,7 @@ func (h *Handler) handleNewAlert(rule *ruleModel.Rule, event *AlertDedupEvent) e return err } -func (h *Handler) logStats(rule *ruleModel.Rule, event *AlertDedupEvent) { +func (h *Handler) logStats(rule *ruleModel.Rule, event *alertApiModels.AlertDedupEvent) { h.MetricsLogger.Log( []metrics.Dimension{ {Name: "Severity", Value: getSeverity(rule, event)}, @@ -123,15 +124,15 @@ func (h *Handler) logStats(rule *ruleModel.Rule, event *AlertDedupEvent) { ) } -func (h *Handler) updateExistingAlert(event *AlertDedupEvent) error { +func (h *Handler) updateExistingAlert(event *alertApiModels.AlertDedupEvent) error { // When updating alert, we need to update only 3 fields // - The number of events included in the alert // - The log types of the events in the alert // - The alert update time updateExpression := expression. - Set(expression.Name(alertTableEventCountAttribute), expression.Value(event.EventCount)). - Set(expression.Name(alertTableLogTypesAttribute), expression.Value(event.LogTypes)). - Set(expression.Name(alertTableUpdateTimeAttribute), expression.Value(event.UpdateTime)) + Set(expression.Name(alertApiModels.AlertTableEventCountAttribute), expression.Value(event.EventCount)). + Set(expression.Name(alertApiModels.AlertTableLogTypesAttribute), expression.Value(event.LogTypes)). + Set(expression.Name(alertApiModels.AlertTableUpdateTimeAttribute), expression.Value(event.UpdateTime)) expr, err := expression.NewBuilder().WithUpdate(updateExpression).Build() if err != nil { return errors.Wrap(err, "failed to build update expression") @@ -143,7 +144,7 @@ func (h *Handler) updateExistingAlert(event *AlertDedupEvent) error { ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), Key: map[string]*dynamodb.AttributeValue{ - alertTablePartitionKey: {S: aws.String(generateAlertID(event))}, + alertApiModels.AlertTablePartitionKey: {S: aws.String(generateAlertID(event))}, }, } @@ -154,8 +155,8 @@ func (h *Handler) updateExistingAlert(event *AlertDedupEvent) error { return nil } -func (h *Handler) storeNewAlert(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) error { - alert := &Alert{ +func (h *Handler) storeNewAlert(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) error { + alert := &alertApiModels.Alert{ ID: generateAlertID(alertDedup), TimePartition: defaultTimePartition, Severity: aws.String(getSeverity(rule, alertDedup)), @@ -163,7 +164,7 @@ func (h *Handler) storeNewAlert(rule *ruleModel.Rule, alertDedup *AlertDedupEven Title: getTitle(rule, alertDedup), FirstEventMatchTime: alertDedup.CreationTime, LogTypes: alertDedup.LogTypes, - AlertDedupEvent: AlertDedupEvent{ + AlertDedupEvent: alertApiModels.AlertDedupEvent{ RuleID: alertDedup.RuleID, RuleVersion: alertDedup.RuleVersion, DeduplicationString: alertDedup.DeduplicationString, @@ -200,7 +201,7 @@ func (h *Handler) storeNewAlert(rule *ruleModel.Rule, alertDedup *AlertDedupEven return nil } -func (h *Handler) sendAlertNotification(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) error { +func (h *Handler) sendAlertNotification(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) error { alertNotification := &alertModel.Alert{ AlertID: aws.String(generateAlertID(alertDedup)), AnalysisID: alertDedup.RuleID, @@ -248,7 +249,7 @@ func (h *Handler) sendAlertNotification(rule *ruleModel.Rule, alertDedup *AlertD return nil } -func getTitle(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) string { +func getTitle(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) string { if alertDedup.GeneratedTitle != nil { return *alertDedup.GeneratedTitle } @@ -259,35 +260,35 @@ func getTitle(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) string { return rule.ID } -func getDescription(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) string { +func getDescription(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) string { if alertDedup.GeneratedDescription != nil { return *alertDedup.GeneratedDescription } return rule.Description } -func getReference(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) string { +func getReference(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) string { if alertDedup.GeneratedReference != nil { return *alertDedup.GeneratedReference } return rule.Reference } -func getRunbook(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) string { +func getRunbook(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) string { if alertDedup.GeneratedRunbook != nil { return *alertDedup.GeneratedRunbook } return rule.Runbook } -func getSeverity(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) string { +func getSeverity(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) string { if alertDedup.GeneratedSeverity != nil { return *alertDedup.GeneratedSeverity } return string(rule.Severity) } -func getOutputIds(rule *ruleModel.Rule, alertDedup *AlertDedupEvent) []string { +func getOutputIds(rule *ruleModel.Rule, alertDedup *alertApiModels.AlertDedupEvent) []string { if alertDedup.GeneratedDestinations != nil { if len(alertDedup.GeneratedDestinations) == 0 { return skipOutput @@ -304,7 +305,7 @@ func getRuleDisplayName(rule *ruleModel.Rule) *string { return nil } -func generateAlertID(event *AlertDedupEvent) string { +func generateAlertID(event *alertApiModels.AlertDedupEvent) string { key := event.RuleID + ":" + strconv.FormatInt(event.AlertCount, 10) + ":" + event.DeduplicationString keyHash := md5.Sum([]byte(key)) // nolint(gosec) return hex.EncodeToString(keyHash[:]) diff --git a/internal/log_analysis/alert_forwarder/forwarder/forwarder_test.go b/internal/log_analysis/alert_forwarder/forwarder/forwarder_test.go index 75a49243bb..e701b21771 100644 --- a/internal/log_analysis/alert_forwarder/forwarder/forwarder_test.go +++ b/internal/log_analysis/alert_forwarder/forwarder/forwarder_test.go @@ -36,13 +36,14 @@ import ( ruleModel "github.com/panther-labs/panther/api/lambda/analysis/models" alertModel "github.com/panther-labs/panther/api/lambda/delivery/models" + alertApiModels "github.com/panther-labs/panther/internal/log_analysis/alerts_api/models" "github.com/panther-labs/panther/pkg/gatewayapi" "github.com/panther-labs/panther/pkg/metrics" "github.com/panther-labs/panther/pkg/testutils" ) var ( - oldAlertDedupEvent = &AlertDedupEvent{ + oldAlertDedupEvent = &alertApiModels.AlertDedupEvent{ RuleID: "ruleId", RuleVersion: "ruleVersion", DeduplicationString: "dedupString", @@ -55,7 +56,7 @@ var ( GeneratedTitle: aws.String("test title"), } - newAlertDedupEvent = &AlertDedupEvent{ + newAlertDedupEvent = &alertApiModels.AlertDedupEvent{ RuleID: oldAlertDedupEvent.RuleID, RuleVersion: oldAlertDedupEvent.RuleVersion, DeduplicationString: oldAlertDedupEvent.DeduplicationString, @@ -131,7 +132,7 @@ func TestHandleStoreAndSendNotification(t *testing.T) { sqsMock.On("SendMessage", expectedSendMessageInput).Return(&sqs.SendMessageOutput{}, nil) - expectedAlert := &Alert{ + expectedAlert := &alertApiModels.Alert{ ID: "b25dc23fb2a0b362da8428dbec1381a8", TimePartition: "defaultPartition", Severity: aws.String(string(testRuleResponse.Severity)), @@ -139,7 +140,7 @@ func TestHandleStoreAndSendNotification(t *testing.T) { Title: aws.StringValue(newAlertDedupEvent.GeneratedTitle), FirstEventMatchTime: newAlertDedupEvent.CreationTime, LogTypes: newAlertDedupEvent.LogTypes, - AlertDedupEvent: AlertDedupEvent{ + AlertDedupEvent: alertApiModels.AlertDedupEvent{ RuleID: newAlertDedupEvent.RuleID, Type: newAlertDedupEvent.Type, RuleVersion: newAlertDedupEvent.RuleVersion, @@ -190,7 +191,7 @@ func TestHandleStoreAndSendNotificationNoRuleDisplayNameNoTitle(t *testing.T) { MetricsLogger: metricsMock, } - newAlertDedupEventWithoutTitle := &AlertDedupEvent{ + newAlertDedupEventWithoutTitle := &alertApiModels.AlertDedupEvent{ RuleID: oldAlertDedupEvent.RuleID, RuleVersion: oldAlertDedupEvent.RuleVersion, DeduplicationString: oldAlertDedupEvent.DeduplicationString, @@ -234,14 +235,14 @@ func TestHandleStoreAndSendNotificationNoRuleDisplayNameNoTitle(t *testing.T) { sqsMock.On("SendMessage", expectedSendMessageInput).Return(&sqs.SendMessageOutput{}, nil) - expectedAlert := &Alert{ + expectedAlert := &alertApiModels.Alert{ ID: "b25dc23fb2a0b362da8428dbec1381a8", TimePartition: "defaultPartition", Severity: aws.String(string(testRuleResponse.Severity)), Title: newAlertDedupEventWithoutTitle.RuleID, FirstEventMatchTime: newAlertDedupEventWithoutTitle.CreationTime, LogTypes: newAlertDedupEvent.LogTypes, - AlertDedupEvent: AlertDedupEvent{ + AlertDedupEvent: alertApiModels.AlertDedupEvent{ RuleID: newAlertDedupEventWithoutTitle.RuleID, RuleVersion: newAlertDedupEventWithoutTitle.RuleVersion, LogTypes: newAlertDedupEventWithoutTitle.LogTypes, @@ -318,7 +319,7 @@ func TestHandleStoreAndSendNotificationNoGeneratedTitle(t *testing.T) { http.StatusOK, nil, testRuleResponse).Once() sqsMock.On("SendMessage", expectedSendMessageInput).Return(&sqs.SendMessageOutput{}, nil) - expectedAlert := &Alert{ + expectedAlert := &alertApiModels.Alert{ ID: "b25dc23fb2a0b362da8428dbec1381a8", TimePartition: "defaultPartition", Severity: aws.String(string(testRuleResponse.Severity)), @@ -326,7 +327,7 @@ func TestHandleStoreAndSendNotificationNoGeneratedTitle(t *testing.T) { Title: "DisplayName", FirstEventMatchTime: newAlertDedupEvent.CreationTime, LogTypes: newAlertDedupEvent.LogTypes, - AlertDedupEvent: AlertDedupEvent{ + AlertDedupEvent: alertApiModels.AlertDedupEvent{ RuleID: newAlertDedupEvent.RuleID, RuleVersion: newAlertDedupEvent.RuleVersion, LogTypes: newAlertDedupEvent.LogTypes, @@ -351,7 +352,7 @@ func TestHandleStoreAndSendNotificationNoGeneratedTitle(t *testing.T) { TableName: aws.String("alertsTable"), } - dedupEventWithoutTitle := &AlertDedupEvent{ + dedupEventWithoutTitle := &alertApiModels.AlertDedupEvent{ RuleID: newAlertDedupEvent.RuleID, RuleVersion: newAlertDedupEvent.RuleVersion, Type: newAlertDedupEvent.Type, @@ -415,7 +416,7 @@ func TestHandleStoreAndSendNotificationNilOldDedup(t *testing.T) { sqsMock.On("SendMessage", expectedSendMessageInput).Return(&sqs.SendMessageOutput{}, nil) - expectedAlert := &Alert{ + expectedAlert := &alertApiModels.Alert{ ID: "b25dc23fb2a0b362da8428dbec1381a8", TimePartition: "defaultPartition", Severity: aws.String(string(testRuleResponse.Severity)), @@ -423,7 +424,7 @@ func TestHandleStoreAndSendNotificationNilOldDedup(t *testing.T) { RuleDisplayName: &testRuleResponse.DisplayName, FirstEventMatchTime: newAlertDedupEvent.CreationTime, LogTypes: newAlertDedupEvent.LogTypes, - AlertDedupEvent: AlertDedupEvent{ + AlertDedupEvent: alertApiModels.AlertDedupEvent{ RuleID: newAlertDedupEvent.RuleID, Type: newAlertDedupEvent.Type, RuleVersion: newAlertDedupEvent.RuleVersion, @@ -477,7 +478,7 @@ func TestHandleUpdateAlert(t *testing.T) { analysisMock.On("Invoke", expectedGetRuleInput, &ruleModel.Rule{}).Return( http.StatusOK, nil, testRuleResponse).Once() - dedupEventWithUpdatedFields := &AlertDedupEvent{ + dedupEventWithUpdatedFields := &alertApiModels.AlertDedupEvent{ RuleID: newAlertDedupEvent.RuleID, RuleVersion: newAlertDedupEvent.RuleVersion, DeduplicationString: newAlertDedupEvent.DeduplicationString, @@ -537,8 +538,7 @@ func TestHandleUpdateAlertDDBError(t *testing.T) { } analysisMock.On("Invoke", expectedGetRuleInput, &ruleModel.Rule{}).Return( http.StatusOK, nil, testRuleResponse).Once() - - dedupEventWithUpdatedFields := &AlertDedupEvent{ + dedupEventWithUpdatedFields := &alertApiModels.AlertDedupEvent{ RuleID: newAlertDedupEvent.RuleID, RuleVersion: newAlertDedupEvent.RuleVersion, DeduplicationString: newAlertDedupEvent.DeduplicationString, @@ -623,7 +623,7 @@ func TestHandleDontConsiderThresholdInRuleErrors(t *testing.T) { } ruleErrorDedup := *newAlertDedupEvent - ruleErrorDedup.Type = RuleErrorType + ruleErrorDedup.Type = alertModel.RuleErrorType analysisMock.On("Invoke", expectedGetRuleInput, &ruleModel.Rule{}).Return( http.StatusOK, nil, ruleWithThreshold).Once() @@ -663,7 +663,7 @@ func TestHandleShouldCreateAlertIfThresholdNowReached(t *testing.T) { Threshold: 1000, } - newAlertDedup := &AlertDedupEvent{ + newAlertDedup := &alertApiModels.AlertDedupEvent{ RuleID: oldAlertDedupEvent.RuleID, Type: oldAlertDedupEvent.Type, RuleVersion: oldAlertDedupEvent.RuleVersion, diff --git a/internal/log_analysis/alert_forwarder/main/lambda.go b/internal/log_analysis/alert_forwarder/main/lambda.go index 1f25a9b580..20a26957d8 100644 --- a/internal/log_analysis/alert_forwarder/main/lambda.go +++ b/internal/log_analysis/alert_forwarder/main/lambda.go @@ -28,6 +28,7 @@ import ( "go.uber.org/zap" "github.com/panther-labs/panther/internal/log_analysis/alert_forwarder/forwarder" + alertApiModels "github.com/panther-labs/panther/internal/log_analysis/alerts_api/models" "github.com/panther-labs/panther/internal/log_analysis/log_processor/common" "github.com/panther-labs/panther/pkg/lambdalogger" "github.com/panther-labs/panther/pkg/metrics" @@ -80,14 +81,14 @@ func reporterHandler(lc *lambdacontext.LambdaContext, event events.DynamoDBEvent // Note that if there is an error in processing any of the messages in the batch, the whole batch will be retried. for _, record := range event.Records { - oldAlertDedupEvent, unmarshalErr := forwarder.FromDynamodDBAttribute(record.Change.OldImage) + oldAlertDedupEvent, unmarshalErr := alertApiModels.FromDynamodDBAttribute(record.Change.OldImage) if unmarshalErr != nil { operation.LogError(errors.Wrapf(err, "failed to unmarshal item")) // continuing since there is nothing we can do here continue } - newAlertDedupEvent, unmarshalErr := forwarder.FromDynamodDBAttribute(record.Change.NewImage) + newAlertDedupEvent, unmarshalErr := alertApiModels.FromDynamodDBAttribute(record.Change.NewImage) if unmarshalErr != nil { operation.LogError(errors.Wrapf(err, "failed to unmarshal item")) // continuing since there is nothing we can do here diff --git a/internal/log_analysis/alerts_api/api/get_alert_test.go b/internal/log_analysis/alerts_api/api/get_alert_test.go index 5c760db4e0..2ba09e9392 100644 --- a/internal/log_analysis/alerts_api/api/get_alert_test.go +++ b/internal/log_analysis/alerts_api/api/get_alert_test.go @@ -114,7 +114,68 @@ func TestGetAlertDoesNotExist(t *testing.T) { require.NoError(t, err) } -func TestGetAlert(t *testing.T) { +func TestGetPolicyAlert(t *testing.T) { + api, tableMock, s3Mock := initTest() + + timeNow := time.Date(2020, 1, 1, 1, 59, 0, 0, time.UTC) + + input := &models.GetAlertInput{ + AlertID: "alertId", + EventsPageSize: aws.Int(1), + } + + alertItem := &table.AlertItem{ + Type: "POLICY", + AlertID: "alertId", + RuleID: "AWS.S3.Bucket.Encryption", + PolicyID: "AWS.S3.Bucket.Encryption", + PolicyVersion: "L.iGYcpTHTS2sQF5VUBuO9Ompm7bTLwc", + Status: "", + CreationTime: timeNow, + UpdateTime: timeNow, + Severity: "INFO", + ResourceTypes: []string{"AWS.S3.Bucket"}, + ResourceID: "arn:aws:s3:::panther-integration-processeddata-test-20201103180759", + LastUpdatedBy: "userId", + LastUpdatedByTime: time.Date(2020, 1, 1, 1, 59, 0, 0, time.UTC), + } + + expectedSummary := &models.AlertSummary{ + AlertID: "alertId", + RuleID: aws.String("AWS.S3.Bucket.Encryption"), + PolicyID: "AWS.S3.Bucket.Encryption", + Status: "OPEN", + Type: "POLICY", + PolicyVersion: "L.iGYcpTHTS2sQF5VUBuO9Ompm7bTLwc", + RuleVersion: aws.String(""), + DedupString: aws.String(""), + Severity: aws.String("INFO"), + Title: aws.String("arn:aws:s3:::panther-integration-processeddata-test-20201103180759"), + CreationTime: aws.Time(timeNow), + UpdateTime: aws.Time(timeNow), + EventsMatched: aws.Int(0), + LogTypes: []string{}, + ResourceTypes: []string{"AWS.S3.Bucket"}, + ResourceID: "arn:aws:s3:::panther-integration-processeddata-test-20201103180759", + LastUpdatedBy: "userId", + LastUpdatedByTime: time.Date(2020, 1, 1, 1, 59, 0, 0, time.UTC), + DeliveryResponses: []*models.DeliveryResponse{}, + } + + tableMock.On("GetAlert", "alertId").Return(alertItem, nil).Once() + + result, err := api.GetAlert(input) + require.NoError(t, err) + require.Equal(t, &models.GetAlertOutput{ + AlertSummary: *expectedSummary, + Events: []*string{}, + EventsLastEvaluatedKey: aws.String("eyJsb2dUeXBlVG9Ub2tlbiI6e319"), + }, result) + s3Mock.AssertExpectations(t) + tableMock.AssertExpectations(t) +} + +func TestGetRuleAlert(t *testing.T) { api, tableMock, s3Mock := initTest() // The S3 object keys returned by S3 List objects command @@ -155,6 +216,8 @@ func TestGetAlert(t *testing.T) { UpdateTime: aws.Time(time.Date(2020, 1, 1, 1, 59, 0, 0, time.UTC)), EventsMatched: aws.Int(5), LogTypes: []string{"logtype"}, + ResourceTypes: []string{}, + ResourceID: "", LastUpdatedBy: "userId", LastUpdatedByTime: time.Date(2020, 1, 1, 1, 59, 0, 0, time.UTC), DeliveryResponses: []*models.DeliveryResponse{}, @@ -297,6 +360,8 @@ func TestGetAlertFilterOutS3KeysOutsideTheTimePeriod(t *testing.T) { Severity: aws.String("INFO"), DedupString: aws.String("dedupString"), LogTypes: []string{"logtype"}, + ResourceTypes: []string{}, + ResourceID: "", LastUpdatedBy: "userId", LastUpdatedByTime: time.Date(2020, 1, 1, 1, 59, 0, 0, time.UTC), DeliveryResponses: []*models.DeliveryResponse{}, diff --git a/internal/log_analysis/alerts_api/api/list_alerts_test.go b/internal/log_analysis/alerts_api/api/list_alerts_test.go index 74e1246270..673efc83ee 100644 --- a/internal/log_analysis/alerts_api/api/list_alerts_test.go +++ b/internal/log_analysis/alerts_api/api/list_alerts_test.go @@ -43,6 +43,8 @@ var ( Severity: "INFO", DedupString: "dedupString", LogTypes: []string{"AWS.CloudTrail"}, + ResourceTypes: []string{"AWS.ResourceType"}, + ResourceID: "resourceId", EventCount: 100, RuleVersion: "ruleVersion", RuleDisplayName: aws.String("ruleDisplayName"), @@ -68,6 +70,8 @@ var ( EventsMatched: aws.Int(100), Title: aws.String("title"), LogTypes: []string{"AWS.CloudTrail"}, + ResourceTypes: []string{"AWS.ResourceType"}, + ResourceID: "resourceId", LastUpdatedBy: "userId", LastUpdatedByTime: timeInTest, DeliveryResponses: []*models.DeliveryResponse{}, @@ -145,6 +149,8 @@ func TestListAllAlertsWithoutTitle(t *testing.T) { Severity: "INFO", DedupString: "dedupString", LogTypes: []string{"AWS.CloudTrail"}, + ResourceTypes: []string{"AWS.ResourceType"}, + ResourceID: "resourceId", EventCount: 100, RuleVersion: "ruleVersion", LastUpdatedBy: "userId", @@ -159,6 +165,8 @@ func TestListAllAlertsWithoutTitle(t *testing.T) { Severity: "INFO", DedupString: "dedupString", LogTypes: []string{"AWS.CloudTrail"}, + ResourceTypes: []string{"AWS.ResourceType"}, + ResourceID: "resourceId", EventCount: 100, RuleVersion: "ruleVersion", RuleDisplayName: aws.String("ruleDisplayName"), @@ -181,6 +189,8 @@ func TestListAllAlertsWithoutTitle(t *testing.T) { EventsMatched: aws.Int(100), Title: aws.String("ruleId"), LogTypes: []string{"AWS.CloudTrail"}, + ResourceTypes: []string{"AWS.ResourceType"}, + ResourceID: "resourceId", LastUpdatedBy: "userId", LastUpdatedByTime: timeInTest, DeliveryResponses: []*models.DeliveryResponse{}, @@ -201,6 +211,8 @@ func TestListAllAlertsWithoutTitle(t *testing.T) { // we return the display name Title: aws.String("ruleDisplayName"), LogTypes: []string{"AWS.CloudTrail"}, + ResourceTypes: []string{"AWS.ResourceType"}, + ResourceID: "resourceId", LastUpdatedBy: "userId", LastUpdatedByTime: timeInTest, DeliveryResponses: []*models.DeliveryResponse{}, diff --git a/internal/log_analysis/alert_forwarder/forwarder/models.go b/internal/log_analysis/alerts_api/models/models.go similarity index 89% rename from internal/log_analysis/alert_forwarder/forwarder/models.go rename to internal/log_analysis/alerts_api/models/models.go index d46c697458..8136e8787b 100644 --- a/internal/log_analysis/alert_forwarder/forwarder/models.go +++ b/internal/log_analysis/alerts_api/models/models.go @@ -1,4 +1,4 @@ -package forwarder +package models /** * Panther is a Cloud-Native SIEM for the Modern Security Team. @@ -27,13 +27,10 @@ import ( ) const ( - // The type of an Alert that is triggered because of a rule encountering an error - RuleErrorType = "RULE_ERROR" - - alertTablePartitionKey = "id" - alertTableLogTypesAttribute = "logTypes" - alertTableEventCountAttribute = "eventCount" - alertTableUpdateTimeAttribute = "updateTime" + AlertTablePartitionKey = "id" + AlertTableLogTypesAttribute = "logTypes" + AlertTableEventCountAttribute = "eventCount" + AlertTableUpdateTimeAttribute = "updateTime" ) // AlertDedupEvent represents the event stored in the alert dedup DDB table by the rules engine @@ -57,6 +54,16 @@ type AlertDedupEvent struct { AlertCount int64 `dynamodbav:"-"` // There is no need to store this item in DDB } +// AlertPolicy represents the policy-specific fields for alerts genereated by policies +type AlertPolicy struct { + PolicyID string `dynamodbav:"policyId,string"` + PolicyDisplayName string `dynamodbav:"policyDisplayName,string"` + PolicyVersion string `dynamodbav:"policyVersion,string"` + PolicySourceID string `dynamodbav:"policySourceId,string"` + ResourceTypes []string `dynamodbav:"resourceTypes,stringset"` + ResourceID string `dynamodbav:"resourceId,string"` // This is the failing resource +} + // Alert contains all the fields associated to the alert stored in DDB type Alert struct { ID string `dynamodbav:"id,string"` @@ -68,6 +75,7 @@ type Alert struct { // Alert Title - will be the Python-generated title or a default one if no Python-generated title is available. Title string `dynamodbav:"title,string"` AlertDedupEvent + AlertPolicy } func FromDynamodDBAttribute(input map[string]events.DynamoDBAttributeValue) (event *AlertDedupEvent, err error) { diff --git a/internal/log_analysis/alert_forwarder/forwarder/models_test.go b/internal/log_analysis/alerts_api/models/models_test.go similarity index 99% rename from internal/log_analysis/alert_forwarder/forwarder/models_test.go rename to internal/log_analysis/alerts_api/models/models_test.go index 85d4d85de5..b2b98738d7 100644 --- a/internal/log_analysis/alert_forwarder/forwarder/models_test.go +++ b/internal/log_analysis/alerts_api/models/models_test.go @@ -1,4 +1,4 @@ -package forwarder +package models /** * Panther is a Cloud-Native SIEM for the Modern Security Team. diff --git a/internal/log_analysis/alerts_api/table/list.go b/internal/log_analysis/alerts_api/table/list.go index 480d74cad8..d214836e0a 100644 --- a/internal/log_analysis/alerts_api/table/list.go +++ b/internal/log_analysis/alerts_api/table/list.go @@ -100,8 +100,6 @@ func (table *AlertsTable) ListAll(input *models.ListAlertsInput) ( // Perform post-filtering data returned from ddb alert = filterByTitleContains(input, alert) - alert = filterByRuleIDContains(input, alert) - alert = filterByAlertIDContains(input, alert) if alert != nil { summaries = append(summaries, alert) @@ -232,6 +230,7 @@ func (table *AlertsTable) applyFilters(builder *expression.Builder, input *model filterByStatus(&filter, input) filterByEventCount(&filter, input) filterByLogType(&filter, input) + filterByResourceType(&filter, input) filterByType(&filter, input) // Finally, overwrite the existing condition filter on the builder @@ -303,60 +302,118 @@ func filterByLogType(filter *expression.ConditionBuilder, input *models.ListAler } } +// filterByResourceType - filters by list of resource types +func filterByResourceType(filter *expression.ConditionBuilder, input *models.ListAlertsInput) { + if len(input.ResourceTypes) > 0 { + // Start with the first known key + multiFilter := expression.Name(ResourceTypesKey).Contains(input.ResourceTypes[0]) + + // Then add or conditions starting at a new slice from the second index + for _, resourceType := range input.ResourceTypes[1:] { + multiFilter = multiFilter.Or(expression.Name(ResourceTypesKey).Contains(resourceType)) + } + + *filter = filter.And(multiFilter) + } +} + // filterByType - filters by the type of the alert func filterByType(filter *expression.ConditionBuilder, input *models.ListAlertsInput) { - if len(input.Type) > 0 { + if len(input.Types) > 0 { + // Start with the first known key var multiFilter expression.ConditionBuilder - switch input.Type { - case alertdeliverymodels.RuleErrorType: - multiFilter = expression.Equal(expression.Name(TypeKey), expression.Value(input.Type)) - case alertdeliverymodels.RuleType: - // Alerts for rule matches don't always have the attribute specified + + // Rule errors don't always have the attribute specified for backwards compatibility + if input.Types[0] == alertdeliverymodels.RuleErrorType { multiFilter = expression. - Equal(expression.Name(TypeKey), expression.Value(input.Type)). - Or(expression.Name(TypeKey).AttributeNotExists()) - default: - panic("Uknown type :" + input.Type) + Or( + expression.AttributeNotExists(expression.Name(TypeKey)), + expression.Equal(expression.Name(TypeKey), expression.Value(input.Types[0])), + ) + } else { + multiFilter = expression.Name(TypeKey).Equal(expression.Value(input.Types[0])) + } + + // Then add or conditions starting at a new slice from the second index + for _, alertType := range input.Types[1:] { + // Rule errors don't always have the attribute specified for backwards compatibility + if alertType == alertdeliverymodels.RuleErrorType { + multiFilter = multiFilter. + Or( + expression.AttributeNotExists(expression.Name(TypeKey)), + expression.Equal(expression.Name(TypeKey), expression.Value(alertType)), + ) + } else { + multiFilter = multiFilter.Or(expression.Name(TypeKey).Equal(expression.Value(alertType))) + } } *filter = filter.And(multiFilter) } } -// filterByTitleContains - filters by a name that contains a string (case insensitive) +// filterByTitleContains - filters alerts by a name that contains a string (case insensitive) against multiple fields func filterByTitleContains(input *models.ListAlertsInput, alert *AlertItem) *AlertItem { - if alert != nil && input.NameContains != nil && alert.Title == "" && !strings.Contains( + // If we don't have a search string, return the alert + if input.NameContains == nil { + return alert + } + + lowerNameContains := strings.ToLower(*input.NameContains) + + // Common across all alert types, we see if it matches an alert title + if alert.Title != "" && strings.Contains( strings.ToLower(alert.Title), - strings.ToLower(*input.NameContains), + lowerNameContains, ) { + return alert + } + + // Check for non-policy types in this order: RuleDisplayName, RuleID + if alert.Type != alertdeliverymodels.PolicyType { + if alert.RuleDisplayName != nil && strings.Contains( + strings.ToLower(*alert.RuleDisplayName), + lowerNameContains, + ) { + + return alert + } + + if strings.Contains( + strings.ToLower(alert.RuleID), + lowerNameContains, + ) { + + return alert + } return nil } - return alert -} + // Check for policy types in this order: ResourceID, PolicyDisplayName, PolicyID + if strings.Contains( + strings.ToLower(alert.ResourceID), + lowerNameContains, + ) { -// filterByRuleIDContains - filters by a name that contains a string (case insensitive) -func filterByRuleIDContains(input *models.ListAlertsInput, alert *AlertItem) *AlertItem { - if alert != nil && input.RuleIDContains != nil && !strings.Contains( - strings.ToLower(alert.RuleID), - strings.ToLower(*input.RuleIDContains), + return alert + } + + if strings.Contains( + strings.ToLower(alert.PolicyDisplayName), + lowerNameContains, ) { - return nil + return alert } - return alert -} -// filterByAlertIDContains - filters by a name that contains a string (case insensitive) -func filterByAlertIDContains(input *models.ListAlertsInput, alert *AlertItem) *AlertItem { - if alert != nil && input.AlertIDContains != nil && !strings.Contains( - strings.ToLower(alert.AlertID), - strings.ToLower(*input.AlertIDContains), + if strings.Contains( + strings.ToLower(alert.PolicyID), + lowerNameContains, ) { - return nil + return alert } - return alert + return nil } // filterByEventCount - filters by an eventCount defined by a range of two numbers diff --git a/internal/log_analysis/alerts_api/table/table.go b/internal/log_analysis/alerts_api/table/table.go index 7e47be5075..8cdba018ff 100644 --- a/internal/log_analysis/alerts_api/table/table.go +++ b/internal/log_analysis/alerts_api/table/table.go @@ -39,6 +39,7 @@ const ( EventCountKey = "eventCount" StatusKey = "status" LogTypesKey = "logTypes" + ResourceTypesKey = "resourceTypes" DeliveryResponsesKey = "deliveryResponses" LastUpdatedByKey = "lastUpdatedBy" LastUpdatedByTimeKey = "lastUpdatedByTime" @@ -109,4 +110,11 @@ type AlertItem struct { LastUpdatedBy string `json:"lastUpdatedBy"` // LastUpdatedByTime - stores the timestamp of the last person who modified the Alert LastUpdatedByTime time.Time `json:"lastUpdatedByTime"` + // Policy related fields + PolicyID string `json:"policyId"` + PolicyDisplayName string `json:"policyDisplayName"` + PolicySourceID string `json:"policySourceId"` + PolicyVersion string `json:"policyVersion"` + ResourceTypes []string `json:"resourceTypes"` + ResourceID string `json:"resourceId"` } diff --git a/internal/log_analysis/alerts_api/utils/utils.go b/internal/log_analysis/alerts_api/utils/utils.go index e782d79a4a..a1f8f6c978 100644 --- a/internal/log_analysis/alerts_api/utils/utils.go +++ b/internal/log_analysis/alerts_api/utils/utils.go @@ -67,6 +67,12 @@ func AlertItemToSummary(item *table.AlertItem) *models.AlertSummary { LastUpdatedByTime: item.LastUpdatedByTime, UpdateTime: &item.UpdateTime, DeliveryResponses: item.DeliveryResponses, + PolicyID: item.PolicyID, + PolicyDisplayName: item.PolicyDisplayName, + PolicySourceID: item.PolicySourceID, + PolicyVersion: item.PolicyVersion, + ResourceTypes: item.ResourceTypes, + ResourceID: item.ResourceID, } } @@ -76,8 +82,17 @@ func GetAlertTitle(alert *table.AlertItem) *string { if alert.Title != "" { return aws.String(alert.Title) } - if alert.RuleDisplayName != nil { - return alert.RuleDisplayName + if alert.Type != alertdeliverymodels.PolicyType { + if alert.RuleDisplayName != nil { + return alert.RuleDisplayName + } + return &alert.RuleID } - return &alert.RuleID + if alert.ResourceID != "" { + return &alert.ResourceID + } + if alert.PolicyDisplayName != "" { + return &alert.PolicyDisplayName + } + return &alert.PolicyID } diff --git a/web/__generated__/schema.tsx b/web/__generated__/schema.tsx index b8bda8f715..db31e45c35 100644 --- a/web/__generated__/schema.tsx +++ b/web/__generated__/schema.tsx @@ -18,6 +18,7 @@ import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; export type Maybe = T | null; +export type Omit = Pick>; export type RequireFields = { [X in Exclude]?: T[X] } & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ @@ -110,12 +111,10 @@ export type Alert = { alertId: Scalars['ID']; creationTime: Scalars['AWSDateTime']; deliveryResponses: Array>; - eventsMatched: Scalars['Int']; - ruleId?: Maybe; severity: SeverityEnum; status: AlertStatusesEnum; title: Scalars['String']; - logTypes: Array; + type: AlertTypesEnum; lastUpdatedBy?: Maybe; lastUpdatedByTime?: Maybe; updateTime: Scalars['AWSDateTime']; @@ -126,16 +125,23 @@ export type AlertDetails = Alert & { alertId: Scalars['ID']; creationTime: Scalars['AWSDateTime']; deliveryResponses: Array>; - eventsMatched: Scalars['Int']; - ruleId?: Maybe; severity: SeverityEnum; status: AlertStatusesEnum; title: Scalars['String']; type: AlertTypesEnum; - logTypes: Array; lastUpdatedBy?: Maybe; lastUpdatedByTime?: Maybe; updateTime: Scalars['AWSDateTime']; + detection: AlertDetailsDetectionInfo; +}; + +export type AlertDetailsDetectionInfo = AlertDetailsRuleInfo | AlertSummaryPolicyInfo; + +export type AlertDetailsRuleInfo = { + __typename?: 'AlertDetailsRuleInfo'; + ruleId?: Maybe; + logTypes: Array; + eventsMatched: Scalars['Int']; dedupString: Scalars['String']; events: Array; eventsLastEvaluatedKey?: Maybe; @@ -153,21 +159,37 @@ export type AlertSummary = Alert & { alertId: Scalars['ID']; creationTime: Scalars['AWSDateTime']; deliveryResponses: Array>; - eventsMatched: Scalars['Int']; - ruleId?: Maybe; type: AlertTypesEnum; severity: SeverityEnum; status: AlertStatusesEnum; title: Scalars['String']; - logTypes: Array; lastUpdatedBy?: Maybe; lastUpdatedByTime?: Maybe; updateTime: Scalars['AWSDateTime']; + detection: AlertSummaryDetectionInfo; +}; + +export type AlertSummaryDetectionInfo = AlertSummaryRuleInfo | AlertSummaryPolicyInfo; + +export type AlertSummaryPolicyInfo = { + __typename?: 'AlertSummaryPolicyInfo'; + policyId?: Maybe; + resourceId?: Maybe; + policySourceId: Scalars['String']; + resourceTypes: Array; +}; + +export type AlertSummaryRuleInfo = { + __typename?: 'AlertSummaryRuleInfo'; + ruleId?: Maybe; + logTypes: Array; + eventsMatched: Scalars['Int']; }; export enum AlertTypesEnum { Rule = 'RULE', RuleError = 'RULE_ERROR', + Policy = 'POLICY', } export type AsanaConfig = { @@ -480,12 +502,11 @@ export type ListAlertsInput = { exclusiveStartKey?: Maybe; severity?: Maybe>>; logTypes?: Maybe>; - type?: Maybe; + resourceTypes?: Maybe>; + types?: Maybe>; nameContains?: Maybe; createdAtBefore?: Maybe; createdAtAfter?: Maybe; - ruleIdContains?: Maybe; - alertIdContains?: Maybe; status?: Maybe>>; eventCountMin?: Maybe; eventCountMax?: Maybe; @@ -1507,7 +1528,9 @@ export type ResolversTypes = { ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; String: ResolverTypeWrapper; - AlertDetails: ResolverTypeWrapper; + AlertDetails: ResolverTypeWrapper< + Omit & { detection: ResolversTypes['AlertDetailsDetectionInfo'] } + >; Alert: ResolversTypes['AlertDetails'] | ResolversTypes['AlertSummary']; AWSDateTime: ResolverTypeWrapper; DeliveryResponse: ResolverTypeWrapper; @@ -1515,12 +1538,23 @@ export type ResolversTypes = { SeverityEnum: SeverityEnum; AlertStatusesEnum: AlertStatusesEnum; AlertTypesEnum: AlertTypesEnum; + AlertDetailsDetectionInfo: + | ResolversTypes['AlertDetailsRuleInfo'] + | ResolversTypes['AlertSummaryPolicyInfo']; + AlertDetailsRuleInfo: ResolverTypeWrapper; AWSJSON: ResolverTypeWrapper; + AlertSummaryPolicyInfo: ResolverTypeWrapper; ListAlertsInput: ListAlertsInput; ListAlertsSortFieldsEnum: ListAlertsSortFieldsEnum; SortDirEnum: SortDirEnum; ListAlertsResponse: ResolverTypeWrapper; - AlertSummary: ResolverTypeWrapper; + AlertSummary: ResolverTypeWrapper< + Omit & { detection: ResolversTypes['AlertSummaryDetectionInfo'] } + >; + AlertSummaryDetectionInfo: + | ResolversTypes['AlertSummaryRuleInfo'] + | ResolversTypes['AlertSummaryPolicyInfo']; + AlertSummaryRuleInfo: ResolverTypeWrapper; SendTestAlertInput: SendTestAlertInput; Destination: ResolverTypeWrapper; DestinationTypeEnum: DestinationTypeEnum; @@ -1661,7 +1695,9 @@ export type ResolversParentTypes = { ID: Scalars['ID']; Int: Scalars['Int']; String: Scalars['String']; - AlertDetails: AlertDetails; + AlertDetails: Omit & { + detection: ResolversParentTypes['AlertDetailsDetectionInfo']; + }; Alert: ResolversParentTypes['AlertDetails'] | ResolversParentTypes['AlertSummary']; AWSDateTime: Scalars['AWSDateTime']; DeliveryResponse: DeliveryResponse; @@ -1669,12 +1705,23 @@ export type ResolversParentTypes = { SeverityEnum: SeverityEnum; AlertStatusesEnum: AlertStatusesEnum; AlertTypesEnum: AlertTypesEnum; + AlertDetailsDetectionInfo: + | ResolversParentTypes['AlertDetailsRuleInfo'] + | ResolversParentTypes['AlertSummaryPolicyInfo']; + AlertDetailsRuleInfo: AlertDetailsRuleInfo; AWSJSON: Scalars['AWSJSON']; + AlertSummaryPolicyInfo: AlertSummaryPolicyInfo; ListAlertsInput: ListAlertsInput; ListAlertsSortFieldsEnum: ListAlertsSortFieldsEnum; SortDirEnum: SortDirEnum; ListAlertsResponse: ListAlertsResponse; - AlertSummary: AlertSummary; + AlertSummary: Omit & { + detection: ResolversParentTypes['AlertSummaryDetectionInfo']; + }; + AlertSummaryDetectionInfo: + | ResolversParentTypes['AlertSummaryRuleInfo'] + | ResolversParentTypes['AlertSummaryPolicyInfo']; + AlertSummaryRuleInfo: AlertSummaryRuleInfo; SendTestAlertInput: SendTestAlertInput; Destination: Destination; DestinationTypeEnum: DestinationTypeEnum; @@ -1831,12 +1878,10 @@ export type AlertResolvers< ParentType, ContextType >; - eventsMatched?: Resolver; - ruleId?: Resolver, ParentType, ContextType>; severity?: Resolver; status?: Resolver; title?: Resolver; - logTypes?: Resolver, ParentType, ContextType>; + type?: Resolver; lastUpdatedBy?: Resolver, ParentType, ContextType>; lastUpdatedByTime?: Resolver, ParentType, ContextType>; updateTime?: Resolver; @@ -1853,16 +1898,35 @@ export type AlertDetailsResolvers< ParentType, ContextType >; - eventsMatched?: Resolver; - ruleId?: Resolver, ParentType, ContextType>; severity?: Resolver; status?: Resolver; title?: Resolver; type?: Resolver; - logTypes?: Resolver, ParentType, ContextType>; lastUpdatedBy?: Resolver, ParentType, ContextType>; lastUpdatedByTime?: Resolver, ParentType, ContextType>; updateTime?: Resolver; + detection?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type AlertDetailsDetectionInfoResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['AlertDetailsDetectionInfo'] = ResolversParentTypes['AlertDetailsDetectionInfo'] +> = { + __resolveType: TypeResolveFn< + 'AlertDetailsRuleInfo' | 'AlertSummaryPolicyInfo', + ParentType, + ContextType + >; +}; + +export type AlertDetailsRuleInfoResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['AlertDetailsRuleInfo'] = ResolversParentTypes['AlertDetailsRuleInfo'] +> = { + ruleId?: Resolver, ParentType, ContextType>; + logTypes?: Resolver, ParentType, ContextType>; + eventsMatched?: Resolver; dedupString?: Resolver; events?: Resolver, ParentType, ContextType>; eventsLastEvaluatedKey?: Resolver, ParentType, ContextType>; @@ -1880,16 +1944,46 @@ export type AlertSummaryResolvers< ParentType, ContextType >; - eventsMatched?: Resolver; - ruleId?: Resolver, ParentType, ContextType>; type?: Resolver; severity?: Resolver; status?: Resolver; title?: Resolver; - logTypes?: Resolver, ParentType, ContextType>; lastUpdatedBy?: Resolver, ParentType, ContextType>; lastUpdatedByTime?: Resolver, ParentType, ContextType>; updateTime?: Resolver; + detection?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type AlertSummaryDetectionInfoResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['AlertSummaryDetectionInfo'] = ResolversParentTypes['AlertSummaryDetectionInfo'] +> = { + __resolveType: TypeResolveFn< + 'AlertSummaryRuleInfo' | 'AlertSummaryPolicyInfo', + ParentType, + ContextType + >; +}; + +export type AlertSummaryPolicyInfoResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['AlertSummaryPolicyInfo'] = ResolversParentTypes['AlertSummaryPolicyInfo'] +> = { + policyId?: Resolver, ParentType, ContextType>; + resourceId?: Resolver, ParentType, ContextType>; + policySourceId?: Resolver; + resourceTypes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type AlertSummaryRuleInfoResolvers< + ContextType = any, + ParentType extends ResolversParentTypes['AlertSummaryRuleInfo'] = ResolversParentTypes['AlertSummaryRuleInfo'] +> = { + ruleId?: Resolver, ParentType, ContextType>; + logTypes?: Resolver, ParentType, ContextType>; + eventsMatched?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; @@ -3084,7 +3178,12 @@ export type Resolvers = { ActiveSuppressCount?: ActiveSuppressCountResolvers; Alert?: AlertResolvers; AlertDetails?: AlertDetailsResolvers; + AlertDetailsDetectionInfo?: AlertDetailsDetectionInfoResolvers; + AlertDetailsRuleInfo?: AlertDetailsRuleInfoResolvers; AlertSummary?: AlertSummaryResolvers; + AlertSummaryDetectionInfo?: AlertSummaryDetectionInfoResolvers; + AlertSummaryPolicyInfo?: AlertSummaryPolicyInfoResolvers; + AlertSummaryRuleInfo?: AlertSummaryRuleInfoResolvers; AsanaConfig?: AsanaConfigResolvers; AWSDateTime?: GraphQLScalarType; AWSEmail?: GraphQLScalarType; diff --git a/web/__tests__/__mocks__/builders.generated.ts b/web/__tests__/__mocks__/builders.generated.ts index 743fb878e6..ec3597f2bc 100644 --- a/web/__tests__/__mocks__/builders.generated.ts +++ b/web/__tests__/__mocks__/builders.generated.ts @@ -25,7 +25,10 @@ import { AddS3LogIntegrationInput, AddSqsLogIntegrationInput, AlertDetails, + AlertDetailsRuleInfo, AlertSummary, + AlertSummaryPolicyInfo, + AlertSummaryRuleInfo, AsanaConfig, AsanaConfigInput, ComplianceIntegration, @@ -141,7 +144,9 @@ import { UploadPoliciesResponse, User, AccountTypeEnum, + AlertDetailsDetectionInfo, AlertStatusesEnum, + AlertSummaryDetectionInfo, AlertTypesEnum, ComplianceStatusEnum, DestinationTypeEnum, @@ -262,13 +267,10 @@ export const buildAlertDetails = (overrides: Partial = {}): AlertD creationTime: 'creationTime' in overrides ? overrides.creationTime : '2020-10-28T02:06:29.865Z', deliveryResponses: 'deliveryResponses' in overrides ? overrides.deliveryResponses : [buildDeliveryResponse()], - eventsMatched: 'eventsMatched' in overrides ? overrides.eventsMatched : 516, - ruleId: 'ruleId' in overrides ? overrides.ruleId : '9ad2c6da-417d-414f-a3e5-7959acdeaa9e', severity: 'severity' in overrides ? overrides.severity : SeverityEnum.Critical, status: 'status' in overrides ? overrides.status : AlertStatusesEnum.Closed, title: 'title' in overrides ? overrides.title : 'Steel', type: 'type' in overrides ? overrides.type : AlertTypesEnum.Rule, - logTypes: 'logTypes' in overrides ? overrides.logTypes : ['Books'], lastUpdatedBy: 'lastUpdatedBy' in overrides ? overrides.lastUpdatedBy @@ -276,10 +278,22 @@ export const buildAlertDetails = (overrides: Partial = {}): AlertD lastUpdatedByTime: 'lastUpdatedByTime' in overrides ? overrides.lastUpdatedByTime : '2020-07-02T20:00:23.050Z', updateTime: 'updateTime' in overrides ? overrides.updateTime : '2020-02-22T04:54:35.910Z', - dedupString: 'dedupString' in overrides ? overrides.dedupString : 'Auto Loan Account', - events: 'events' in overrides ? overrides.events : ['"bar"'], + detection: 'detection' in overrides ? overrides.detection : buildAlertDetailsRuleInfo(), + }; +}; + +export const buildAlertDetailsRuleInfo = ( + overrides: Partial = {} +): AlertDetailsRuleInfo => { + return { + __typename: 'AlertDetailsRuleInfo', + ruleId: 'ruleId' in overrides ? overrides.ruleId : '17db7258-2d08-4d56-b993-666b8e6db65e', + logTypes: 'logTypes' in overrides ? overrides.logTypes : ['Baht'], + eventsMatched: 'eventsMatched' in overrides ? overrides.eventsMatched : 545, + dedupString: 'dedupString' in overrides ? overrides.dedupString : 'panel', + events: 'events' in overrides ? overrides.events : ['"car"'], eventsLastEvaluatedKey: - 'eventsLastEvaluatedKey' in overrides ? overrides.eventsLastEvaluatedKey : 'Accountability', + 'eventsLastEvaluatedKey' in overrides ? overrides.eventsLastEvaluatedKey : 'index', }; }; @@ -290,13 +304,10 @@ export const buildAlertSummary = (overrides: Partial = {}): AlertS creationTime: 'creationTime' in overrides ? overrides.creationTime : '2020-08-08T12:15:31.121Z', deliveryResponses: 'deliveryResponses' in overrides ? overrides.deliveryResponses : [buildDeliveryResponse()], - eventsMatched: 'eventsMatched' in overrides ? overrides.eventsMatched : 670, - ruleId: 'ruleId' in overrides ? overrides.ruleId : '6eb9c948-5a13-4955-bd91-b98801b55bed', - type: 'type' in overrides ? overrides.type : AlertTypesEnum.Rule, + type: 'type' in overrides ? overrides.type : AlertTypesEnum.RuleError, severity: 'severity' in overrides ? overrides.severity : SeverityEnum.Medium, status: 'status' in overrides ? overrides.status : AlertStatusesEnum.Triaged, title: 'title' in overrides ? overrides.title : 'indexing', - logTypes: 'logTypes' in overrides ? overrides.logTypes : ['Costa Rica'], lastUpdatedBy: 'lastUpdatedBy' in overrides ? overrides.lastUpdatedBy @@ -304,6 +315,30 @@ export const buildAlertSummary = (overrides: Partial = {}): AlertS lastUpdatedByTime: 'lastUpdatedByTime' in overrides ? overrides.lastUpdatedByTime : '2020-07-29T23:42:06.903Z', updateTime: 'updateTime' in overrides ? overrides.updateTime : '2020-09-17T19:32:46.882Z', + detection: 'detection' in overrides ? overrides.detection : buildAlertSummaryRuleInfo(), + }; +}; + +export const buildAlertSummaryPolicyInfo = ( + overrides: Partial = {} +): AlertSummaryPolicyInfo => { + return { + __typename: 'AlertSummaryPolicyInfo', + policyId: 'policyId' in overrides ? overrides.policyId : 'a68babd7-7c1c-4dee-a33e-b8009e6d8017', + resourceId: 'resourceId' in overrides ? overrides.resourceId : '5th generation', + policySourceId: 'policySourceId' in overrides ? overrides.policySourceId : 'program', + resourceTypes: 'resourceTypes' in overrides ? overrides.resourceTypes : ['brand'], + }; +}; + +export const buildAlertSummaryRuleInfo = ( + overrides: Partial = {} +): AlertSummaryRuleInfo => { + return { + __typename: 'AlertSummaryRuleInfo', + ruleId: 'ruleId' in overrides ? overrides.ruleId : '8780849b-30b8-4ce2-934b-bf033369b110', + logTypes: 'logTypes' in overrides ? overrides.logTypes : ['Personal Loan Account'], + eventsMatched: 'eventsMatched' in overrides ? overrides.eventsMatched : 240, }; }; @@ -767,14 +802,13 @@ export const buildListAlertsInput = (overrides: Partial = {}): 'exclusiveStartKey' in overrides ? overrides.exclusiveStartKey : 'Throughway', severity: 'severity' in overrides ? overrides.severity : [SeverityEnum.Low], logTypes: 'logTypes' in overrides ? overrides.logTypes : ['Awesome Wooden Mouse'], - type: 'type' in overrides ? overrides.type : AlertTypesEnum.Rule, + resourceTypes: 'resourceTypes' in overrides ? overrides.resourceTypes : ['24 hour'], + types: 'types' in overrides ? overrides.types : [AlertTypesEnum.Policy], nameContains: 'nameContains' in overrides ? overrides.nameContains : 'Island', createdAtBefore: 'createdAtBefore' in overrides ? overrides.createdAtBefore : '2020-05-22T12:33:45.819Z', createdAtAfter: 'createdAtAfter' in overrides ? overrides.createdAtAfter : '2020-04-26T13:02:02.091Z', - ruleIdContains: 'ruleIdContains' in overrides ? overrides.ruleIdContains : 'virtual', - alertIdContains: 'alertIdContains' in overrides ? overrides.alertIdContains : 'Garden', status: 'status' in overrides ? overrides.status : [AlertStatusesEnum.Open], eventCountMin: 'eventCountMin' in overrides ? overrides.eventCountMin : 694, eventCountMax: 'eventCountMax' in overrides ? overrides.eventCountMax : 911, diff --git a/web/src/components/Editor/__mocks__/Editor.tsx b/web/src/components/Editor/__mocks__/Editor.tsx index 86e1c36d0a..ee8ddd6154 100644 --- a/web/src/components/Editor/__mocks__/Editor.tsx +++ b/web/src/components/Editor/__mocks__/Editor.tsx @@ -16,15 +16,6 @@ * along with this program. If not, see . */ -/** - * Copyright (C) 2020 Panther Labs Inc - * - * Panther Enterprise is licensed under the terms of a commercial license available from - * Panther Labs Inc ("Panther Commercial License") by contacting contact@runpanther.com. - * All use, distribution, and/or modification of this software, whether commercial or non-commercial, - * falls under the Panther Commercial License to the extent it is permitted. - */ - import React from 'react'; const Editor: React.FC = ({ minLines, mode, onChange, ...rest }) => { diff --git a/web/src/components/Editor/__mocks__/index.tsx b/web/src/components/Editor/__mocks__/index.tsx index 9166dce738..a183365068 100644 --- a/web/src/components/Editor/__mocks__/index.tsx +++ b/web/src/components/Editor/__mocks__/index.tsx @@ -16,13 +16,4 @@ * along with this program. If not, see . */ -/** - * Copyright (C) 2020 Panther Labs Inc - * - * Panther Enterprise is licensed under the terms of a commercial license available from - * Panther Labs Inc ("Panther Commercial License") by contacting contact@runpanther.com. - * All use, distribution, and/or modification of this software, whether commercial or non-commercial, - * falls under the Panther Commercial License to the extent it is permitted. - */ - export { default } from './Editor'; diff --git a/web/src/components/cards/AlertCard/AlertCard.test.tsx b/web/src/components/cards/AlertCard/AlertCard.test.tsx index c9a7868ac8..40385e7db3 100644 --- a/web/src/components/cards/AlertCard/AlertCard.test.tsx +++ b/web/src/components/cards/AlertCard/AlertCard.test.tsx @@ -24,68 +24,76 @@ import { waitForElementToBeRemoved, } from 'test-utils'; import React from 'react'; -import { AlertStatusesEnum, DestinationTypeEnum, SeverityEnum } from 'Generated/schema'; +import { + AlertStatusesEnum, + AlertSummaryRuleInfo, + DestinationTypeEnum, + SeverityEnum, +} from 'Generated/schema'; import urls from 'Source/urls'; import { mockListDestinations } from 'Source/graphql/queries'; import AlertCard from './index'; describe('AlertCard', () => { it('should match snapshot', async () => { - const alertData = buildAlertSummary(); + const alert = buildAlertSummary(); - const { container } = render(); + const { container } = render(); expect(container).toMatchSnapshot(); }); it('displays the correct Alert data in the card', async () => { - const alertData = buildAlertSummary(); + const alert = buildAlertSummary(); + const detectionData = alert.detection as AlertSummaryRuleInfo; - const { getByText, getByAriaLabel } = render(); + const { getByText, getByAriaLabel } = render(); - expect(getByText(alertData.title)).toBeInTheDocument(); - expect(getByAriaLabel(`Link to rule ${alertData.ruleId}`)).toBeInTheDocument(); + expect(getByText(alert.title)).toBeInTheDocument(); + expect(getByAriaLabel(`Link to rule ${detectionData.ruleId}`)).toBeInTheDocument(); expect(getByText('Events')).toBeInTheDocument(); expect(getByText('Destinations')).toBeInTheDocument(); - expect(getByAriaLabel(`Creation time for ${alertData.alertId}`)).toBeInTheDocument(); + expect(getByAriaLabel(`Creation time for ${alert.alertId}`)).toBeInTheDocument(); expect(getByText(SeverityEnum.Medium)).toBeInTheDocument(); expect(getByText(AlertStatusesEnum.Triaged)).toBeInTheDocument(); expect(getByAriaLabel('Change Alert Status')).toBeInTheDocument(); }); it('should not display link to Rule', async () => { - const alertData = buildAlertSummary(); + const alert = buildAlertSummary(); + const detectionData = alert.detection as AlertSummaryRuleInfo; - const { queryByText, queryByAriaLabel } = render( - - ); + const { queryByText, queryByAriaLabel } = render(); - expect(queryByText(alertData.title)).toBeInTheDocument(); - expect(queryByAriaLabel(`Link to rule ${alertData.ruleId}`)).not.toBeInTheDocument(); + expect(queryByText(alert.title)).toBeInTheDocument(); + expect(queryByAriaLabel(`Link to rule ${detectionData.ruleId}`)).not.toBeInTheDocument(); }); it('should check links are valid', async () => { - const alertData = buildAlertSummary(); - const { getByAriaLabel } = render(); + const alert = buildAlertSummary(); + const detectionData = alert.detection as AlertSummaryRuleInfo; + + const { getByAriaLabel } = render(); + expect(getByAriaLabel('Link to Alert')).toHaveAttribute( 'href', - urls.logAnalysis.alerts.details(alertData.alertId) + urls.logAnalysis.alerts.details(alert.alertId) ); - expect(getByAriaLabel(`Link to rule ${alertData.ruleId}`)).toHaveAttribute( + expect(getByAriaLabel(`Link to rule ${detectionData.ruleId}`)).toHaveAttribute( 'href', - urls.logAnalysis.rules.details(alertData.ruleId) + urls.logAnalysis.rules.details(detectionData.ruleId) ); }); it('should render alert destinations logos', async () => { const outputId = 'destination-of-alert'; - const alertData = buildAlertSummary({ + const alert = buildAlertSummary({ deliveryResponses: [buildDeliveryResponse({ outputId })], }); const destination = buildDestination({ outputId, outputType: DestinationTypeEnum.Slack }); const mocks = [mockListDestinations({ data: { destinations: [destination] } })]; - const { getByAriaLabel, getByAltText } = render(, { + const { getByAriaLabel, getByAltText } = render(, { mocks, }); const loadingInterfaceElement = getByAriaLabel('Loading...'); @@ -96,13 +104,13 @@ describe('AlertCard', () => { it('should render message that destination delivery is failing', async () => { const outputId = 'destination-of-alert'; - const alertData = buildAlertSummary({ + const alert = buildAlertSummary({ deliveryResponses: [buildDeliveryResponse({ outputId, success: false })], }); const destination = buildDestination({ outputId, outputType: DestinationTypeEnum.Slack }); const mocks = [mockListDestinations({ data: { destinations: [destination] } })]; - const { getByAriaLabel, getByAltText } = render(, { + const { getByAriaLabel, getByAltText } = render(, { mocks, }); const loadingInterfaceElement = getByAriaLabel('Loading...'); diff --git a/web/src/components/cards/AlertCard/AlertCard.tsx b/web/src/components/cards/AlertCard/AlertCard.tsx index ef8bffdace..a6c5988fa9 100644 --- a/web/src/components/cards/AlertCard/AlertCard.tsx +++ b/web/src/components/cards/AlertCard/AlertCard.tsx @@ -18,7 +18,7 @@ import GenericItemCard from 'Components/GenericItemCard'; import { Flex, Icon, Link, Text, Box } from 'pouncejs'; -import { AlertTypesEnum } from 'Generated/schema'; +import { AlertDetailsRuleInfo, AlertTypesEnum } from 'Generated/schema'; import { Link as RRLink } from 'react-router-dom'; import SeverityBadge from 'Components/badges/SeverityBadge'; import React from 'react'; @@ -48,6 +48,7 @@ const AlertCard: React.FC = ({ alert, }); + const detectionData = alert.detection as AlertDetailsRuleInfo; return ( @@ -87,11 +88,11 @@ const AlertCard: React.FC = ({ value={ - {alert.ruleId} + {detectionData.ruleId} } @@ -105,11 +106,13 @@ const AlertCard: React.FC = ({ /> } + value={} /> diff --git a/web/src/components/cards/AlertCard/__snapshots__/AlertCard.test.tsx.snap b/web/src/components/cards/AlertCard/__snapshots__/AlertCard.test.tsx.snap index d281f1d0d6..fc5e108ace 100644 --- a/web/src/components/cards/AlertCard/__snapshots__/AlertCard.test.tsx.snap +++ b/web/src/components/cards/AlertCard/__snapshots__/AlertCard.test.tsx.snap @@ -119,7 +119,7 @@ exports[`AlertCard should match snapshot 1`] = ` .panther-5 { font-size: 0.75rem; - color: #d64242; + color: #09a084; } .panther-37 { @@ -283,7 +283,7 @@ exports[`AlertCard should match snapshot 1`] = ` .panther-20 { width: 12px; height: 12px; - background-color: hsl(353, 43%, 63%); + background-color: hsl(221, 31%, 31%); border-radius: 99999px; } @@ -491,7 +491,7 @@ exports[`AlertCard should match snapshot 1`] = ` - Rule Match + Rule Error
- 6eb9c948-5a13-4955-bd91-b98801b55bed + 8780849b-30b8-4ce2-934b-bf033369b110
- Costa Rica + Personal Loan Account
@@ -605,16 +605,16 @@ exports[`AlertCard should match snapshot 1`] = ` class="panther-12" >
Events
- 670 + 240
. */ -/** - * Copyright (C) 2020 Panther Labs Inc - * - * Panther Enterprise is licensed under the terms of a commercial license available from - * Panther Labs Inc ("Panther Commercial License") by contacting contact@runpanther.com. - * All use, distribution, and/or modification of this software, whether commercial or non-commercial, - * falls under the Panther Commercial License to the extent it is permitted. - */ - import React from 'react'; import { useListDestinationsAndDefaults } from 'Pages/ListDestinations'; import { extractErrorMessage } from 'Helpers/utils'; diff --git a/web/src/graphql/fragments/AlertDetailsFull.generated.ts b/web/src/graphql/fragments/AlertDetailsFull.generated.ts index 24b42cbf58..b07f086777 100644 --- a/web/src/graphql/fragments/AlertDetailsFull.generated.ts +++ b/web/src/graphql/fragments/AlertDetailsFull.generated.ts @@ -25,42 +25,62 @@ import gql from 'graphql-tag'; export type AlertDetailsFull = Pick< Types.AlertDetails, | 'alertId' - | 'ruleId' | 'type' | 'title' | 'creationTime' - | 'eventsMatched' | 'updateTime' - | 'eventsLastEvaluatedKey' - | 'events' - | 'dedupString' | 'severity' | 'status' - | 'logTypes' | 'lastUpdatedBy' | 'lastUpdatedByTime' -> & { deliveryResponses: Array> }; +> & { + deliveryResponses: Array>; + detection: + | Pick< + Types.AlertDetailsRuleInfo, + | 'ruleId' + | 'logTypes' + | 'eventsMatched' + | 'eventsLastEvaluatedKey' + | 'events' + | 'dedupString' + > + | Pick< + Types.AlertSummaryPolicyInfo, + 'policyId' | 'resourceTypes' | 'resourceId' | 'policySourceId' + >; +}; export const AlertDetailsFull = gql` fragment AlertDetailsFull on AlertDetails { alertId - ruleId type title creationTime deliveryResponses { ...DeliveryResponseFull } - eventsMatched updateTime - eventsLastEvaluatedKey - events - dedupString severity status - logTypes lastUpdatedBy lastUpdatedByTime + detection { + ... on AlertSummaryPolicyInfo { + policyId + resourceTypes + resourceId + policySourceId + } + ... on AlertDetailsRuleInfo { + ruleId + logTypes + eventsMatched + eventsLastEvaluatedKey + events + dedupString + } + } } ${DeliveryResponseFull} `; diff --git a/web/src/graphql/fragments/AlertDetailsFull.graphql b/web/src/graphql/fragments/AlertDetailsFull.graphql index 78014be47f..c8d1576d95 100644 --- a/web/src/graphql/fragments/AlertDetailsFull.graphql +++ b/web/src/graphql/fragments/AlertDetailsFull.graphql @@ -1,20 +1,30 @@ fragment AlertDetailsFull on AlertDetails { alertId - ruleId type title creationTime deliveryResponses { ...DeliveryResponseFull } - eventsMatched updateTime - eventsLastEvaluatedKey - events - dedupString severity status - logTypes lastUpdatedBy lastUpdatedByTime + detection { + ... on AlertSummaryPolicyInfo { + policyId + resourceTypes + resourceId + policySourceId + } + ... on AlertDetailsRuleInfo { + ruleId + logTypes + eventsMatched + eventsLastEvaluatedKey + events + dedupString + } + } } diff --git a/web/src/graphql/fragments/AlertSummaryFull.generated.ts b/web/src/graphql/fragments/AlertSummaryFull.generated.ts index 4a5093a19a..15b1cb715c 100644 --- a/web/src/graphql/fragments/AlertSummaryFull.generated.ts +++ b/web/src/graphql/fragments/AlertSummaryFull.generated.ts @@ -25,23 +25,27 @@ import gql from 'graphql-tag'; export type AlertSummaryFull = Pick< Types.AlertSummary, | 'alertId' - | 'ruleId' | 'title' | 'severity' | 'type' | 'status' | 'creationTime' - | 'eventsMatched' | 'updateTime' - | 'logTypes' | 'lastUpdatedBy' | 'lastUpdatedByTime' -> & { deliveryResponses: Array> }; +> & { + deliveryResponses: Array>; + detection: + | Pick + | Pick< + Types.AlertSummaryPolicyInfo, + 'policyId' | 'resourceTypes' | 'resourceId' | 'policySourceId' + >; +}; export const AlertSummaryFull = gql` fragment AlertSummaryFull on AlertSummary { alertId - ruleId title severity type @@ -50,11 +54,22 @@ export const AlertSummaryFull = gql` deliveryResponses { ...DeliveryResponseFull } - eventsMatched updateTime - logTypes lastUpdatedBy lastUpdatedByTime + detection { + ... on AlertSummaryPolicyInfo { + policyId + resourceTypes + resourceId + policySourceId + } + ... on AlertSummaryRuleInfo { + ruleId + logTypes + eventsMatched + } + } } ${DeliveryResponseFull} `; diff --git a/web/src/graphql/fragments/AlertSummaryFull.graphql b/web/src/graphql/fragments/AlertSummaryFull.graphql index 15bc67a47f..3bf16bb430 100644 --- a/web/src/graphql/fragments/AlertSummaryFull.graphql +++ b/web/src/graphql/fragments/AlertSummaryFull.graphql @@ -1,6 +1,5 @@ fragment AlertSummaryFull on AlertSummary { alertId - ruleId title severity type @@ -9,9 +8,20 @@ fragment AlertSummaryFull on AlertSummary { deliveryResponses { ...DeliveryResponseFull } - eventsMatched updateTime - logTypes lastUpdatedBy lastUpdatedByTime + detection { + ... on AlertSummaryPolicyInfo { + policyId + resourceTypes + resourceId + policySourceId + } + ... on AlertSummaryRuleInfo { + ruleId + logTypes + eventsMatched + } + } } diff --git a/web/src/graphql/fragments/PolicyBasic.generated.ts b/web/src/graphql/fragments/PolicyBasic.generated.ts new file mode 100644 index 0000000000..29ae6d1e9b --- /dev/null +++ b/web/src/graphql/fragments/PolicyBasic.generated.ts @@ -0,0 +1,57 @@ +/** + * Panther is a Cloud-Native SIEM for the Modern Security Team. + * Copyright (C) 2020 Panther Labs Inc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import * as Types from '../../../__generated__/schema'; + +import { GraphQLError } from 'graphql'; +import gql from 'graphql-tag'; + +export type PolicyBasic = Pick< + Types.PolicyDetails, + | 'id' + | 'description' + | 'displayName' + | 'resourceTypes' + | 'complianceStatus' + | 'outputIds' + | 'runbook' + | 'reference' + | 'severity' + | 'tags' + | 'createdAt' + | 'lastModified' + | 'enabled' +>; + +export const PolicyBasic = gql` + fragment PolicyBasic on PolicyDetails { + id + description + displayName + resourceTypes + complianceStatus + outputIds + runbook + reference + severity + tags + createdAt + lastModified + enabled + } +`; diff --git a/web/src/graphql/fragments/PolicyBasic.graphql b/web/src/graphql/fragments/PolicyBasic.graphql new file mode 100644 index 0000000000..3f3e39ff1f --- /dev/null +++ b/web/src/graphql/fragments/PolicyBasic.graphql @@ -0,0 +1,15 @@ +fragment PolicyBasic on PolicyDetails { + id + description + displayName + resourceTypes + complianceStatus + outputIds + runbook + reference + severity + tags + createdAt + lastModified + enabled +} diff --git a/web/src/pages/AlertDetails/AlertDetails.test.tsx b/web/src/pages/AlertDetails/AlertDetails.test.tsx index 372a4102c9..4368c0967a 100644 --- a/web/src/pages/AlertDetails/AlertDetails.test.tsx +++ b/web/src/pages/AlertDetails/AlertDetails.test.tsx @@ -19,6 +19,7 @@ import React from 'react'; import { buildAlertDetails, + buildAlertDetailsRuleInfo, buildAlertSummary, buildDeliveryResponse, buildDestination, @@ -29,6 +30,7 @@ import { } from 'test-utils'; import urls from 'Source/urls'; import { DEFAULT_LARGE_PAGE_SIZE } from 'Source/constants'; +import { AlertDetailsRuleInfo } from 'Generated/schema'; import { Route } from 'react-router-dom'; import { mockListDestinations } from 'Source/graphql/queries'; import { mockAlertDetails } from './graphql/alertDetails.generated'; @@ -40,7 +42,9 @@ describe('AlertDetails', () => { it('renders the correct tab based on a URL param', async () => { const destination = buildDestination(); const alert = buildAlertDetails({ - events: ['"{}"', '"{}"'], + detection: buildAlertDetailsRuleInfo({ + events: ['"{}"', '"{}"'], + }), deliveryResponses: [buildDeliveryResponse({ outputId: destination.outputId })], }); const rule = buildRuleDetails(); @@ -59,7 +63,7 @@ describe('AlertDetails', () => { mockRuleTeaser({ variables: { input: { - id: alert.ruleId, + id: (alert.detection as AlertDetailsRuleInfo).ruleId, }, }, data: { rule }, @@ -94,7 +98,9 @@ describe('AlertDetails', () => { it('correctly lazy loads event tab', async () => { const destination = buildDestination(); const alert = buildAlertDetails({ - events: ['"{}"', '"{}"'], + detection: buildAlertDetailsRuleInfo({ + events: ['"{}"', '"{}"'], + }), deliveryResponses: [buildDeliveryResponse({ outputId: destination.outputId })], }); const rule = buildRuleDetails(); @@ -113,7 +119,7 @@ describe('AlertDetails', () => { mockRuleTeaser({ variables: { input: { - id: alert.ruleId, + id: (alert.detection as AlertDetailsRuleInfo).ruleId, }, }, data: { rule }, @@ -146,7 +152,9 @@ describe('AlertDetails', () => { it('shows destination information in the details tab', async () => { const destination = buildDestination(); const alert = buildAlertDetails({ - events: ['"{}"', '"{}"'], + detection: buildAlertDetailsRuleInfo({ + events: ['"{}"', '"{}"'], + }), deliveryResponses: [buildDeliveryResponse({ outputId: destination.outputId })], }); const rule = buildRuleDetails(); @@ -165,7 +173,7 @@ describe('AlertDetails', () => { mockRuleTeaser({ variables: { input: { - id: alert.ruleId, + id: (alert.detection as AlertDetailsRuleInfo).ruleId, }, }, data: { rule }, @@ -222,7 +230,7 @@ describe('AlertDetails', () => { mockRuleTeaser({ variables: { input: { - id: alert.ruleId, + id: (alert.detection as AlertDetailsRuleInfo).ruleId, }, }, data: { rule }, diff --git a/web/src/pages/AlertDetails/AlertDetails.tsx b/web/src/pages/AlertDetails/AlertDetails.tsx index 04c88a7531..34642a85a0 100644 --- a/web/src/pages/AlertDetails/AlertDetails.tsx +++ b/web/src/pages/AlertDetails/AlertDetails.tsx @@ -30,6 +30,7 @@ import { DEFAULT_LARGE_PAGE_SIZE } from 'Source/constants'; import invert from 'lodash/invert'; import useUrlParams from 'Hooks/useUrlParams'; import { useListDestinations } from 'Source/graphql/queries'; +import { AlertDetailsRuleInfo, AlertTypesEnum } from 'Generated/schema'; import { useAlertDetails } from './graphql/alertDetails.generated'; import { useRuleTeaser } from './graphql/ruleTeaser.generated'; import AlertDetailsBanner from './AlertDetailsBanner'; @@ -69,11 +70,13 @@ const AlertDetailsPage = () => { }, }); + const alertDetectionInfo = alertData?.alert?.detection as AlertDetailsRuleInfo; + const { data: ruleData, loading: ruleLoading } = useRuleTeaser({ - skip: !alertData, + skip: !alertData || alertData.alert?.type === AlertTypesEnum.Policy, variables: { input: { - id: alertData?.alert?.ruleId, + id: alertDetectionInfo?.ruleId, }, }, }); @@ -87,7 +90,7 @@ const AlertDetailsPage = () => { variables: { input: { ...variables.input, - eventsExclusiveStartKey: alertData.alert.eventsLastEvaluatedKey, + eventsExclusiveStartKey: alertDetectionInfo.eventsLastEvaluatedKey, }, }, updateQuery: (previousResult, { fetchMoreResult }) => { @@ -97,7 +100,10 @@ const AlertDetailsPage = () => { alert: { ...previousResult.alert, ...fetchMoreResult.alert, - events: [...previousResult.alert.events, ...fetchMoreResult.alert.events], + events: [ + ...(previousResult.alert.detection as AlertDetailsRuleInfo).events, + ...(fetchMoreResult.alert.detection as AlertDetailsRuleInfo).events, + ], }, }; }, @@ -143,7 +149,7 @@ const AlertDetailsPage = () => { Details - Events ({alertData.alert.eventsMatched}) + Events ({alertDetectionInfo.eventsMatched}) diff --git a/web/src/pages/AlertDetails/AlertDetailsBanner/AlertDetailsBanner.tsx b/web/src/pages/AlertDetails/AlertDetailsBanner/AlertDetailsBanner.tsx index 5958e22b2a..96ec408741 100644 --- a/web/src/pages/AlertDetails/AlertDetailsBanner/AlertDetailsBanner.tsx +++ b/web/src/pages/AlertDetails/AlertDetailsBanner/AlertDetailsBanner.tsx @@ -19,7 +19,7 @@ import { Box, Flex, Heading, Card } from 'pouncejs'; import React from 'react'; import SeverityBadge from 'Components/badges/SeverityBadge'; -import { AlertTypesEnum } from 'Generated/schema'; +import { AlertDetailsRuleInfo, AlertTypesEnum } from 'Generated/schema'; import BulletedLogType from 'Components/BulletedLogType'; import UpdateAlertDropdown from 'Components/dropdowns/UpdateAlertDropdown'; import { AlertSummaryFull } from 'Source/graphql/fragments/AlertSummaryFull.generated'; @@ -79,7 +79,7 @@ const AlertDetailsBanner: React.FC = ({ alert }) => { - {alert.logTypes.map(logType => ( + {(alert.detection as AlertDetailsRuleInfo).logTypes.map(logType => ( ))} diff --git a/web/src/pages/AlertDetails/AlertDetailsBanner/__snapshots__/AlertDetailsBanner.test.tsx.snap b/web/src/pages/AlertDetails/AlertDetailsBanner/__snapshots__/AlertDetailsBanner.test.tsx.snap index f51d21ea6c..724a994f6b 100644 --- a/web/src/pages/AlertDetails/AlertDetailsBanner/__snapshots__/AlertDetailsBanner.test.tsx.snap +++ b/web/src/pages/AlertDetails/AlertDetailsBanner/__snapshots__/AlertDetailsBanner.test.tsx.snap @@ -253,7 +253,7 @@ exports[`AlertDetailsBanner renders 1`] = ` .panther-19 { width: 12px; height: 12px; - background-color: hsl(210, 20%, 70%); + background-color: hsl(323, 43%, 43%); border-radius: 99999px; } @@ -414,7 +414,7 @@ exports[`AlertDetailsBanner renders 1`] = ` - Books + Baht
@@ -555,7 +555,7 @@ exports[`AlertDetailsBanner renders 1`] = ` - Books + Baht diff --git a/web/src/pages/AlertDetails/AlertDetailsEvents/AlertDetailsEvents.tsx b/web/src/pages/AlertDetails/AlertDetailsEvents/AlertDetailsEvents.tsx index 98336268c6..0a2d65fd0c 100644 --- a/web/src/pages/AlertDetails/AlertDetailsEvents/AlertDetailsEvents.tsx +++ b/web/src/pages/AlertDetails/AlertDetailsEvents/AlertDetailsEvents.tsx @@ -19,6 +19,7 @@ import React from 'react'; import JsonViewer from 'Components/JsonViewer'; import { Box, Card, Flex, Heading, Icon, Tooltip } from 'pouncejs'; +import { AlertDetailsRuleInfo } from 'Generated/schema'; import { TableControlsPagination as PaginationControls } from 'Components/utils/TableControls'; import { DEFAULT_LARGE_PAGE_SIZE } from 'Source/constants'; import toPlural from 'Helpers/utils'; @@ -41,18 +42,20 @@ const AlertDetailsEvents: React.FC = ({ alert, fetchMor // 1-based indexing in mind const [eventDisplayIndex, setEventDisplayIndex] = React.useState(1); + const detectionData = alert.detection as AlertDetailsRuleInfo; React.useEffect(() => { - if (eventDisplayIndex - 1 === alert.events.length - DEFAULT_LARGE_PAGE_SIZE) { + if (eventDisplayIndex - 1 === detectionData.events.length - DEFAULT_LARGE_PAGE_SIZE) { fetchMore(); } - }, [eventDisplayIndex, alert.events.length]); + }, [eventDisplayIndex, detectionData.events.length]); return ( - {alert.eventsMatched} Triggered {toPlural('Event', alert.eventsMatched)} + {detectionData.eventsMatched} Triggered{' '} + {toPlural('Event', detectionData.eventsMatched)} = ({ alert, fetchMor - + ); diff --git a/web/src/pages/AlertDetails/AlertDetailsInfo/AlertDetailsInfo.tsx b/web/src/pages/AlertDetails/AlertDetailsInfo/AlertDetailsInfo.tsx index 9b63126b8c..86aa83ed69 100644 --- a/web/src/pages/AlertDetails/AlertDetailsInfo/AlertDetailsInfo.tsx +++ b/web/src/pages/AlertDetails/AlertDetailsInfo/AlertDetailsInfo.tsx @@ -21,6 +21,7 @@ import { Box, Card, Flex, Link, SimpleGrid } from 'pouncejs'; import Linkify from 'Components/Linkify'; import { Link as RRLink } from 'react-router-dom'; import urls from 'Source/urls'; +import { AlertDetailsRuleInfo } from 'Generated/schema'; import { formatDatetime, formatNumber, minutesToString } from 'Helpers/utils'; import { AlertDetails, RuleTeaser } from 'Pages/AlertDetails'; import AlertDeliverySection from 'Pages/AlertDetails/AlertDetailsInfo/AlertDeliverySection'; @@ -35,6 +36,7 @@ interface AlertDetailsInfoProps { const AlertDetailsInfo: React.FC = ({ alert, rule }) => { const { alertDestinations, loading: loadingDestinations } = useAlertDestinations({ alert }); + const detectionData = alert.detection as AlertDetailsRuleInfo; return ( {rule && ( @@ -125,7 +127,7 @@ const AlertDetailsInfo: React.FC = ({ alert, rule }) => { - {alert.dedupString} + {detectionData.dedupString} @@ -168,7 +170,7 @@ const AlertDetailsInfo: React.FC = ({ alert, rule }) => { Deduplication String - {alert.dedupString} + {detectionData.dedupString} )} diff --git a/web/src/pages/AlertDetails/graphql/policyTeaser.generated.ts b/web/src/pages/AlertDetails/graphql/policyTeaser.generated.ts new file mode 100644 index 0000000000..5ca6b2bab3 --- /dev/null +++ b/web/src/pages/AlertDetails/graphql/policyTeaser.generated.ts @@ -0,0 +1,93 @@ +/** + * Panther is a Cloud-Native SIEM for the Modern Security Team. + * Copyright (C) 2020 Panther Labs Inc + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import * as Types from '../../../../__generated__/schema'; + +import { PolicyBasic } from '../../../graphql/fragments/PolicyBasic.generated'; +import { GraphQLError } from 'graphql'; +import gql from 'graphql-tag'; +import * as ApolloReactCommon from '@apollo/client'; +import * as ApolloReactHooks from '@apollo/client'; + +export type PolicyTeaserVariables = { + input: Types.GetPolicyInput; +}; + +export type PolicyTeaser = { policy?: Types.Maybe }; + +export const PolicyTeaserDocument = gql` + query PolicyTeaser($input: GetPolicyInput!) { + policy(input: $input) { + ...PolicyBasic + } + } + ${PolicyBasic} +`; + +/** + * __usePolicyTeaser__ + * + * To run a query within a React component, call `usePolicyTeaser` and pass it any options that fit your needs. + * When your component renders, `usePolicyTeaser` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = usePolicyTeaser({ + * variables: { + * input: // value for 'input' + * }, + * }); + */ +export function usePolicyTeaser( + baseOptions?: ApolloReactHooks.QueryHookOptions +) { + return ApolloReactHooks.useQuery( + PolicyTeaserDocument, + baseOptions + ); +} +export function usePolicyTeaserLazyQuery( + baseOptions?: ApolloReactHooks.LazyQueryHookOptions +) { + return ApolloReactHooks.useLazyQuery( + PolicyTeaserDocument, + baseOptions + ); +} +export type PolicyTeaserHookResult = ReturnType; +export type PolicyTeaserLazyQueryHookResult = ReturnType; +export type PolicyTeaserQueryResult = ApolloReactCommon.QueryResult< + PolicyTeaser, + PolicyTeaserVariables +>; +export function mockPolicyTeaser({ + data, + variables, + errors, +}: { + data: PolicyTeaser; + variables?: PolicyTeaserVariables; + errors?: GraphQLError[]; +}) { + return { + request: { query: PolicyTeaserDocument, variables }, + result: { data, errors }, + }; +} diff --git a/web/src/pages/AlertDetails/graphql/policyTeaser.graphql b/web/src/pages/AlertDetails/graphql/policyTeaser.graphql new file mode 100644 index 0000000000..a48fdbcaec --- /dev/null +++ b/web/src/pages/AlertDetails/graphql/policyTeaser.graphql @@ -0,0 +1,5 @@ +query PolicyTeaser($input: GetPolicyInput!) { + policy(input: $input) { + ...PolicyBasic + } +} diff --git a/web/src/pages/AlertDetails/index.tsx b/web/src/pages/AlertDetails/index.tsx index 26ee137fc4..7082dab9a1 100644 --- a/web/src/pages/AlertDetails/index.tsx +++ b/web/src/pages/AlertDetails/index.tsx @@ -20,3 +20,4 @@ export { default } from './AlertDetails'; export * from './AlertDetails'; export * from './graphql/alertDetails.generated'; export * from './graphql/ruleTeaser.generated'; +export * from './graphql/policyTeaser.generated'; diff --git a/web/src/pages/ListAlerts/ListAlerts.test.tsx b/web/src/pages/ListAlerts/ListAlerts.test.tsx index d87b69dfea..6aa3ba4109 100644 --- a/web/src/pages/ListAlerts/ListAlerts.test.tsx +++ b/web/src/pages/ListAlerts/ListAlerts.test.tsx @@ -28,6 +28,7 @@ import { } from 'test-utils'; import { AlertStatusesEnum, + AlertTypesEnum, ListAlertsSortFieldsEnum, SeverityEnum, SortDirEnum, @@ -91,6 +92,7 @@ describe('ListAlerts', () => { variables: { input: { pageSize: DEFAULT_LARGE_PAGE_SIZE, + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], }, }, data: { @@ -195,6 +197,7 @@ describe('ListAlerts', () => { variables: { input: { pageSize: DEFAULT_LARGE_PAGE_SIZE, + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], }, }, data: { @@ -273,6 +276,7 @@ describe('ListAlerts', () => { `&logTypes[]=${mockedlogType}` + `&nameContains=test` + `&severity[]=${SeverityEnum.Info}&severity[]=${SeverityEnum.Medium}` + + `&type[]=AlertTypesEnum.Rule&type[]=AlertTypesEnum.RuleError` + `&sortBy=${ListAlertsSortFieldsEnum.CreatedAt}&sortDir=${SortDirEnum.Descending}` + `&status[]=${AlertStatusesEnum.Open}&status[]=${AlertStatusesEnum.Triaged}` + `&pageSize=${DEFAULT_LARGE_PAGE_SIZE}`; @@ -288,7 +292,7 @@ describe('ListAlerts', () => { }), mockListAlerts({ variables: { - input: parsedInitialParams, + input: { ...parsedInitialParams, types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError] }, }, data: { alerts: buildListAlertsResponse({ @@ -354,7 +358,7 @@ describe('ListAlerts', () => { }), mockListAlerts({ variables: { - input: parsedInitialParams, + input: { ...parsedInitialParams, types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError] }, }, data: { alerts: buildListAlertsResponse({ @@ -370,6 +374,7 @@ describe('ListAlerts', () => { eventCountMax: 5, severity: [SeverityEnum.Info, SeverityEnum.Medium], status: [AlertStatusesEnum.Open, AlertStatusesEnum.Triaged], + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], }, }, data: { @@ -380,7 +385,7 @@ describe('ListAlerts', () => { }), mockListAlerts({ variables: { - input: parsedInitialParams, + input: { ...parsedInitialParams, types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError] }, }, data: { alerts: buildListAlertsResponse({ @@ -489,6 +494,7 @@ describe('ListAlerts', () => { const initialParams = `?severity[]=${SeverityEnum.Info}&severity[]=${SeverityEnum.Medium}` + `&status[]=${AlertStatusesEnum.Open}&status[]=${AlertStatusesEnum.Triaged}` + + `&type[]=AlertTypesEnum.Rule&type[]=AlertTypesEnum.RuleError` + `&eventCountMin=2` + `&eventCountMax=5` + `&pageSize=${DEFAULT_LARGE_PAGE_SIZE}`; @@ -504,7 +510,7 @@ describe('ListAlerts', () => { }), mockListAlerts({ variables: { - input: parsedInitialParams, + input: { ...parsedInitialParams, types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError] }, }, data: { alerts: buildListAlertsResponse({ @@ -514,7 +520,11 @@ describe('ListAlerts', () => { }), mockListAlerts({ variables: { - input: { ...parsedInitialParams, nameContains: 'test' }, + input: { + ...parsedInitialParams, + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], + nameContains: 'test', + }, }, data: { alerts: buildListAlertsResponse({ @@ -526,6 +536,7 @@ describe('ListAlerts', () => { variables: { input: { ...parsedInitialParams, + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], nameContains: 'test', sortBy: ListAlertsSortFieldsEnum.CreatedAt, sortDir: SortDirEnum.Descending, @@ -541,6 +552,7 @@ describe('ListAlerts', () => { variables: { input: { ...parsedInitialParams, + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], nameContains: 'test', sortBy: ListAlertsSortFieldsEnum.CreatedAt, sortDir: SortDirEnum.Descending, @@ -557,6 +569,7 @@ describe('ListAlerts', () => { variables: { input: { ...parsedInitialParams, + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], nameContains: 'test', sortBy: ListAlertsSortFieldsEnum.CreatedAt, sortDir: SortDirEnum.Descending, diff --git a/web/src/pages/ListAlerts/ListAlerts.tsx b/web/src/pages/ListAlerts/ListAlerts.tsx index 19beb5fd93..a2c4a72a09 100644 --- a/web/src/pages/ListAlerts/ListAlerts.tsx +++ b/web/src/pages/ListAlerts/ListAlerts.tsx @@ -20,7 +20,7 @@ import React from 'react'; import { Alert, Box, Card, Flex, Text } from 'pouncejs'; import { DEFAULT_LARGE_PAGE_SIZE } from 'Source/constants'; import { extractErrorMessage } from 'Helpers/utils'; -import { ListAlertsInput } from 'Generated/schema'; +import { AlertTypesEnum, ListAlertsInput } from 'Generated/schema'; import useInfiniteScroll from 'Hooks/useInfiniteScroll'; import useRequestParamsWithoutPagination from 'Hooks/useRequestParamsWithoutPagination'; import TablePlaceholder from 'Components/TablePlaceholder'; @@ -48,6 +48,7 @@ const ListAlerts = () => { variables: { input: { ...requestParams, + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], pageSize: DEFAULT_LARGE_PAGE_SIZE, }, }, @@ -67,6 +68,8 @@ const ListAlerts = () => { variables: { input: { ...requestParams, + // FIXME: remove this override when we have a multi-select + types: [AlertTypesEnum.Rule, AlertTypesEnum.RuleError], pageSize: DEFAULT_LARGE_PAGE_SIZE, exclusiveStartKey: lastEvaluatedKey, }, diff --git a/web/src/pages/LogAnalysisOverview/LogAnalysisOverview.test.tsx b/web/src/pages/LogAnalysisOverview/LogAnalysisOverview.test.tsx index e52c6fe161..182e18eb9d 100644 --- a/web/src/pages/LogAnalysisOverview/LogAnalysisOverview.test.tsx +++ b/web/src/pages/LogAnalysisOverview/LogAnalysisOverview.test.tsx @@ -18,9 +18,10 @@ import React from 'react'; import MockDate from 'mockdate'; -import { AlertStatusesEnum, SeverityEnum } from 'Generated/schema'; +import { AlertStatusesEnum, AlertSummaryRuleInfo, SeverityEnum } from 'Generated/schema'; import { buildAlertSummary, + buildAlertSummaryRuleInfo, buildListAlertsResponse, buildLogAnalysisMetricsResponse, buildSingleValue, @@ -36,11 +37,30 @@ import { mockGetLogAnalysisMetrics } from './graphql/getLogAnalysisMetrics.gener let defaultMocks: MockedResponse[]; -const recentAlerts = [buildAlertSummary({ alertId: '1', ruleId: 'rule_1' })]; +const recentAlerts = [ + buildAlertSummary({ + alertId: '1', + detection: buildAlertSummaryRuleInfo({ + ruleId: 'rule_1', + }), + }), +]; const highSeverityAlerts = [ - buildAlertSummary({ alertId: '2', ruleId: 'rule_2', severity: SeverityEnum.Critical }), - buildAlertSummary({ alertId: '3', ruleId: 'rule_3', severity: SeverityEnum.High }), + buildAlertSummary({ + alertId: '2', + detection: buildAlertSummaryRuleInfo({ + ruleId: 'rule_2', + }), + severity: SeverityEnum.Critical, + }), + buildAlertSummary({ + alertId: '3', + detection: buildAlertSummaryRuleInfo({ + ruleId: 'rule_3', + }), + severity: SeverityEnum.High, + }), ]; describe('Log Analysis Overview', () => { @@ -143,12 +163,12 @@ describe('Log Analysis Overview', () => { await Promise.all(loadingInterfaceElements.map(ele => waitForElementToBeRemoved(ele))); recentAlerts.forEach(alert => { - expect(getByAriaLabel(`Link to rule ${alert.ruleId}`)); + expect(getByAriaLabel(`Link to rule ${(alert.detection as AlertSummaryRuleInfo).ruleId}`)); }); const topAlertsTabButton = getByText('High Severity Alerts (2)'); fireEvent.click(topAlertsTabButton); highSeverityAlerts.forEach(alert => { - expect(getByAriaLabel(`Link to rule ${alert.ruleId}`)); + expect(getByAriaLabel(`Link to rule ${(alert.detection as AlertSummaryRuleInfo).ruleId}`)); }); }); }); diff --git a/web/src/pages/LogSourceOnboarding/LogSourceCard/LogSourceCard.tsx b/web/src/pages/LogSourceOnboarding/LogSourceCard/LogSourceCard.tsx index a25db16d9f..59da1df7f6 100644 --- a/web/src/pages/LogSourceOnboarding/LogSourceCard/LogSourceCard.tsx +++ b/web/src/pages/LogSourceOnboarding/LogSourceCard/LogSourceCard.tsx @@ -16,15 +16,6 @@ * along with this program. If not, see . */ -/** - * Copyright (C) 2020 Panther Labs Inc - * - * Panther Enterprise is licensed under the terms of a commercial license available from - * Panther Labs Inc ("Panther Commercial License") by contacting contact@runpanther.com. - * All use, distribution, and/or modification of this software, whether commercial or non-commercial, - * falls under the Panther Commercial License to the extent it is permitted. - */ - import * as React from 'react'; import { Box, Flex, Icon, Img, FadeIn } from 'pouncejs'; import { Link as RRLink } from 'react-router-dom'; diff --git a/web/src/pages/LogSourceOnboarding/LogSourceOnboarding.tsx b/web/src/pages/LogSourceOnboarding/LogSourceOnboarding.tsx index 10d1d6510e..f2067bdd09 100644 --- a/web/src/pages/LogSourceOnboarding/LogSourceOnboarding.tsx +++ b/web/src/pages/LogSourceOnboarding/LogSourceOnboarding.tsx @@ -16,15 +16,6 @@ * along with this program. If not, see . */ -/** - * Copyright (C) 2020 Panther Labs Inc - * - * Panther Enterprise is licensed under the terms of a commercial license available from - * Panther Labs Inc ("Panther Commercial License") by contacting contact@runpanther.com. - * All use, distribution, and/or modification of this software, whether commercial or non-commercial, - * falls under the Panther Commercial License to the extent it is permitted. - */ - import React from 'react'; import { Box, Card, FadeIn, SimpleGrid } from 'pouncejs'; import urls from 'Source/urls'; diff --git a/web/src/pages/RuleDetails/RuleAlertsListing/RuleAlertsListing.tsx b/web/src/pages/RuleDetails/RuleAlertsListing/RuleAlertsListing.tsx index 0318a439be..3acdf0416f 100644 --- a/web/src/pages/RuleDetails/RuleAlertsListing/RuleAlertsListing.tsx +++ b/web/src/pages/RuleDetails/RuleAlertsListing/RuleAlertsListing.tsx @@ -36,10 +36,12 @@ import { useListAlertsForRule } from '../graphql/listAlertsForRule.generated'; import Skeleton from './Skeleton'; import { RuleDetailsPageUrlParams } from '../RuleDetails'; -const RuleAlertsListing: React.FC>> = ({ - ruleId, - type, -}) => { +interface RuleAlertsListingProps { + ruleId: string; + type: AlertTypesEnum; +} + +const RuleAlertsListing: React.FC = ({ ruleId, type }) => { const { requestParams } = useRequestParamsWithoutPagination< Omit & RuleDetailsPageUrlParams >(); @@ -52,7 +54,7 @@ const RuleAlertsListing: React.FC { variables: { input: { ruleId: '123', - type: AlertTypesEnum.RuleError, + types: [AlertTypesEnum.RuleError], pageSize: DEFAULT_SMALL_PAGE_SIZE, }, }, @@ -146,7 +147,9 @@ describe('RuleDetails', () => { ...buildListAlertsResponse(), alertSummaries: [ buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Alert 1`, alertId: `alert_1`, type: AlertTypesEnum.Rule, @@ -157,7 +160,7 @@ describe('RuleDetails', () => { variables: { input: { ruleId: '123', - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], pageSize: DEFAULT_SMALL_PAGE_SIZE, }, }, @@ -249,7 +252,9 @@ describe('RuleDetails', () => { ...buildListAlertsResponse(), alertSummaries: [ buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Alert 1`, alertId: `alert_1`, type: AlertTypesEnum.Rule, @@ -260,7 +265,7 @@ describe('RuleDetails', () => { variables: { input: { ruleId: '123', - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], pageSize: DEFAULT_LARGE_PAGE_SIZE, }, }, @@ -316,7 +321,9 @@ describe('RuleDetails', () => { ...buildListAlertsResponse(), alertSummaries: [ buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Error 1`, alertId: `error_1`, type: AlertTypesEnum.RuleError, @@ -327,7 +334,7 @@ describe('RuleDetails', () => { variables: { input: { ruleId: '123', - type: AlertTypesEnum.RuleError, + types: [AlertTypesEnum.RuleError], pageSize: DEFAULT_LARGE_PAGE_SIZE, }, }, @@ -386,7 +393,7 @@ describe('RuleDetails', () => { }, variables: { input: { - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], ruleId: rule.id, pageSize: DEFAULT_LARGE_PAGE_SIZE, }, @@ -439,7 +446,7 @@ describe('RuleDetails', () => { }, variables: { input: { - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], ruleId: rule.id, pageSize: DEFAULT_LARGE_PAGE_SIZE, }, @@ -455,7 +462,7 @@ describe('RuleDetails', () => { variables: { input: { nameContains: 'test', - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], ruleId: rule.id, pageSize: DEFAULT_LARGE_PAGE_SIZE, }, @@ -495,7 +502,9 @@ describe('RuleDetails', () => { ...buildListAlertsResponse(), alertSummaries: [ buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Unique alert ${counter}`, alertId: `alert_${counter}`, type: AlertTypesEnum.Rule, @@ -506,7 +515,7 @@ describe('RuleDetails', () => { variables: { input: { ruleId: '123', - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], pageSize: DEFAULT_LARGE_PAGE_SIZE, ...overrides, }, @@ -593,13 +602,17 @@ describe('RuleDetails', () => { }); const alertSummaries = [ buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Alert 1`, alertId: `alert_1`, type: AlertTypesEnum.Rule, }), buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Alert 2`, alertId: `alert_2`, type: AlertTypesEnum.Rule, @@ -624,7 +637,7 @@ describe('RuleDetails', () => { variables: { input: { ruleId: '123', - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], pageSize: DEFAULT_LARGE_PAGE_SIZE, }, }, @@ -749,13 +762,17 @@ describe('RuleDetails', () => { }); const alertSummaries = [ buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Error 1`, alertId: `error_1`, type: AlertTypesEnum.RuleError, }), buildAlertSummary({ - ruleId: '123', + detection: buildAlertSummaryRuleInfo({ + ruleId: '123', + }), title: `Error 2`, alertId: `error_2`, type: AlertTypesEnum.RuleError, @@ -780,7 +797,7 @@ describe('RuleDetails', () => { variables: { input: { ruleId: '123', - type: AlertTypesEnum.RuleError, + types: [AlertTypesEnum.RuleError], pageSize: DEFAULT_LARGE_PAGE_SIZE, }, }, diff --git a/web/src/pages/RuleDetails/RuleDetails.tsx b/web/src/pages/RuleDetails/RuleDetails.tsx index a2cd6322ad..a8eef155c2 100644 --- a/web/src/pages/RuleDetails/RuleDetails.tsx +++ b/web/src/pages/RuleDetails/RuleDetails.tsx @@ -63,13 +63,11 @@ const RuleDetailsPage: React.FC = () => { }, }); - // dry runs for tabs indicator - const { data: matchesData } = useListAlertsForRule({ fetchPolicy: 'cache-and-network', variables: { input: { - type: AlertTypesEnum.Rule, + types: [AlertTypesEnum.Rule], ruleId: match.params.id, pageSize: DEFAULT_SMALL_PAGE_SIZE, }, @@ -80,7 +78,7 @@ const RuleDetailsPage: React.FC = () => { fetchPolicy: 'cache-and-network', variables: { input: { - type: AlertTypesEnum.RuleError, + types: [AlertTypesEnum.RuleError], ruleId: match.params.id, pageSize: DEFAULT_SMALL_PAGE_SIZE, },