From dea223bfe1678ec314f89b86c727d3c5d1ebf74c Mon Sep 17 00:00:00 2001 From: Selton Fiuza <40501884+seltonfiuza@users.noreply.github.com> Date: Wed, 4 Aug 2021 09:21:36 -0300 Subject: [PATCH] Feature/tra 3349 validation rules merged with develop (#148) * Implemented validation rules, based on: https://up9.atlassian.net/browse/TRA-3349 * Color on Entry based on rules * Background red/green based on rules * Change flag --validation-rules to --test-rules * rules tab UI updated * rules tab font and background-color is changed for objects * Merged with develop * Fixed compilation issues. * Renamed fullEntry -> harEntry where appropriate. * Change green/red logic * Update models.go * Fix latency bug and alignment * Merge Conflicts fix * Working after merge * Working on Nimrod comments * Resolving conflicts * Resolving conflicts * Resolving conflicts * Nimrod Comments pt.3 * Log Error on configmap creation if the user doesn't have permission. * Checking configmap permission to ignore --test-rules * Revert time for mizu to get ready * Nimrod comments pt 4 && merge develop pt3 * Nimrod comments pt 4 && merge develop pt3 * Const rulePolicyPath and filename Co-authored-by: Neim Co-authored-by: nimrod-up9 --- agent/go.mod | 3 +- agent/go.sum | 2 + agent/pkg/api/main.go | 11 +- agent/pkg/api/socket_server_handlers.go | 7 +- agent/pkg/controllers/entries_controller.go | 16 ++- agent/pkg/models/models.go | 70 +++++++--- agent/pkg/rules/models.go | 110 ++++++++++++++++ cli/cmd/tap.go | 4 +- cli/cmd/tapRunner.go | 54 ++++++-- cli/cmd/version.go | 5 +- cli/cmd/viewRunner.go | 3 +- cli/go.sum | 3 + cli/kubernetes/provider.go | 82 +++++++++++- cli/mizu/config.go | 11 +- cli/mizu/configStruct.go | 1 + cli/mizu/configStructs/tapConfig.go | 5 +- cli/mizu/consts.go | 1 + cli/mizu/versionCheck.go | 9 +- shared/consts.go | 2 + shared/go.mod | 3 +- shared/go.sum | 4 + shared/models.go | 82 +++++++++++- tap/har_writer.go | 45 +++---- ui/package-lock.json | 85 +++++++++++++ ui/package.json | 1 + ui/src/components/HarEntry.tsx | 18 ++- ui/src/components/HarEntryDetailed.tsx | 9 +- .../HAREntrySections.module.sass | 22 +++- .../HarEntryViewer/HAREntrySections.tsx | 120 ++++++++++++++++++ .../HarEntryViewer/HAREntryViewer.tsx | 16 ++- ui/src/components/HarPage.tsx | 1 - ui/src/components/style/HarEntry.module.sass | 8 ++ 32 files changed, 716 insertions(+), 97 deletions(-) create mode 100644 agent/pkg/rules/models.go diff --git a/agent/go.mod b/agent/go.mod index 2a5645a2f..290e85d76 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -13,16 +13,17 @@ require ( github.com/go-playground/validator/v10 v10.5.0 github.com/google/martian v2.1.0+incompatible github.com/gorilla/websocket v1.4.2 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 github.com/up9inc/mizu/shared v0.0.0 github.com/up9inc/mizu/tap v0.0.0 + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 go.mongodb.org/mongo-driver v1.5.1 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.8 k8s.io/api v0.21.0 k8s.io/apimachinery v0.21.0 k8s.io/client-go v0.21.0 - github.com/patrickmn/go-cache v2.1.0+incompatible ) replace github.com/up9inc/mizu/shared v0.0.0 => ../shared diff --git a/agent/go.sum b/agent/go.sum index f1d59d0c8..0917a0fbb 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -287,6 +287,8 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/agent/pkg/api/main.go b/agent/pkg/api/main.go index 2e90d8141..70439fc47 100644 --- a/agent/pkg/api/main.go +++ b/agent/pkg/api/main.go @@ -5,10 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/google/martian/har" - "github.com/romana/rlog" - "github.com/up9inc/mizu/tap" - "go.mongodb.org/mongo-driver/bson/primitive" "mizuserver/pkg/holder" "net/url" "os" @@ -17,6 +13,11 @@ import ( "strings" "time" + "github.com/google/martian/har" + "github.com/romana/rlog" + "github.com/up9inc/mizu/tap" + "go.mongodb.org/mongo-driver/bson/primitive" + "mizuserver/pkg/database" "mizuserver/pkg/models" "mizuserver/pkg/resolver" @@ -166,6 +167,8 @@ func saveHarToDb(entry *har.Entry, connectionInfo *tap.ConnectionInfo) { if err := models.GetEntry(&mizuEntry, &baseEntry); err != nil { return } + baseEntry.Rules = models.RunValidationRulesState(*entry, serviceName) + baseEntry.Latency = entry.Timings.Receive baseEntryBytes, _ := models.CreateBaseEntryWebSocketMessage(&baseEntry) BroadcastToBrowserClients(baseEntryBytes) } diff --git a/agent/pkg/api/socket_server_handlers.go b/agent/pkg/api/socket_server_handlers.go index 398f450f8..9e9600fd0 100644 --- a/agent/pkg/api/socket_server_handlers.go +++ b/agent/pkg/api/socket_server_handlers.go @@ -3,13 +3,14 @@ package api import ( "encoding/json" "fmt" - "github.com/romana/rlog" - "github.com/up9inc/mizu/shared" - "github.com/up9inc/mizu/tap" "mizuserver/pkg/models" "mizuserver/pkg/providers" "mizuserver/pkg/up9" "sync" + + "github.com/romana/rlog" + "github.com/up9inc/mizu/shared" + "github.com/up9inc/mizu/tap" ) var browserClientSocketUUIDs = make([]int, 0) diff --git a/agent/pkg/controllers/entries_controller.go b/agent/pkg/controllers/entries_controller.go index 669a64f85..7eda00063 100644 --- a/agent/pkg/controllers/entries_controller.go +++ b/agent/pkg/controllers/entries_controller.go @@ -3,9 +3,6 @@ package controllers import ( "encoding/json" "fmt" - "github.com/gin-gonic/gin" - "github.com/google/martian/har" - "github.com/romana/rlog" "mizuserver/pkg/database" "mizuserver/pkg/models" "mizuserver/pkg/providers" @@ -15,6 +12,10 @@ import ( "net/http" "strings" "time" + + "github.com/gin-gonic/gin" + "github.com/google/martian/har" + "github.com/romana/rlog" ) func GetEntries(c *gin.Context) { @@ -218,7 +219,14 @@ func GetEntry(c *gin.Context) { "msg": "Can't get entry details", }) } - c.JSON(http.StatusOK, fullEntry) + fullEntryWithPolicy := models.FullEntryWithPolicy{} + if err := models.GetEntry(&entryData, &fullEntryWithPolicy); err != nil { + c.JSON(http.StatusInternalServerError, map[string]interface{}{ + "error": true, + "msg": "Can't get entry details", + }) + } + c.JSON(http.StatusOK, fullEntryWithPolicy) } func DeleteAllEntries(c *gin.Context) { diff --git a/agent/pkg/models/models.go b/agent/pkg/models/models.go index adbdf1dab..13013f819 100644 --- a/agent/pkg/models/models.go +++ b/agent/pkg/models/models.go @@ -2,11 +2,14 @@ package models import ( "encoding/json" + + "mizuserver/pkg/rules" + "mizuserver/pkg/utils" + "time" + "github.com/google/martian/har" "github.com/up9inc/mizu/shared" "github.com/up9inc/mizu/tap" - "mizuserver/pkg/utils" - "time" ) type DataUnmarshaler interface { @@ -33,19 +36,33 @@ type MizuEntry struct { ResolvedSource string `json:"resolvedSource,omitempty" gorm:"column:resolvedSource"` ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"` IsOutgoing bool `json:"isOutgoing,omitempty" gorm:"column:isOutgoing"` - EstimatedSizeBytes int `json:"-" gorm:"column:estimatedSizeBytes"` + EstimatedSizeBytes int `json:"-" gorm:"column:estimatedSizeBytes"` } type BaseEntryDetails struct { - Id string `json:"id,omitempty"` - Url string `json:"url,omitempty"` - RequestSenderIp string `json:"requestSenderIp,omitempty"` - Service string `json:"service,omitempty"` - Path string `json:"path,omitempty"` - StatusCode int `json:"statusCode,omitempty"` - Method string `json:"method,omitempty"` - Timestamp int64 `json:"timestamp,omitempty"` - IsOutgoing bool `json:"isOutgoing,omitempty"` + Id string `json:"id,omitempty"` + Url string `json:"url,omitempty"` + RequestSenderIp string `json:"requestSenderIp,omitempty"` + Service string `json:"service,omitempty"` + Path string `json:"path,omitempty"` + StatusCode int `json:"statusCode,omitempty"` + Method string `json:"method,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + IsOutgoing bool `json:"isOutgoing,omitempty"` + Latency int64 `json:"latency,omitempty"` + Rules ApplicableRules `json:"rules,omitempty"` +} + +type ApplicableRules struct { + Latency int64 `json:"latency,omitempty"` + Status bool `json:"status,omitempty"` +} + +func NewApplicableRules(status bool, latency int64) ApplicableRules { + ar := ApplicableRules{} + ar.Status = status + ar.Latency = latency + return ar } type FullEntryDetails struct { @@ -101,11 +118,6 @@ func (fedex *FullEntryDetailsExtra) UnmarshalData(entry *MizuEntry) error { return nil } -type EntryData struct { - Entry string `json:"entry,omitempty"` - ResolvedDestination string `json:"resolvedDestination,omitempty" gorm:"column:resolvedDestination"` -} - type EntriesFilter struct { Limit int `query:"limit" validate:"required,min=1,max=200"` Operator string `query:"operator" validate:"required,oneof='lt' 'gt'"` @@ -186,3 +198,27 @@ type ExtendedCreator struct { *har.Creator Source *string `json:"_source"` } + +type FullEntryWithPolicy struct { + RulesMatched []rules.RulesMatched `json:"rulesMatched,omitempty"` + Entry har.Entry `json:"entry"` + Service string `json:"service"` +} + +func (fewp *FullEntryWithPolicy) UnmarshalData(entry *MizuEntry) error { + if err := json.Unmarshal([]byte(entry.Entry), &fewp.Entry); err != nil { + return err + } + + _, resultPolicyToSend := rules.MatchRequestPolicy(fewp.Entry, entry.Service) + fewp.RulesMatched = resultPolicyToSend + fewp.Service = entry.Service + return nil +} + +func RunValidationRulesState(harEntry har.Entry, service string) ApplicableRules { + numberOfRules, resultPolicyToSend := rules.MatchRequestPolicy(harEntry, service) + statusPolicyToSend, latency := rules.PassedValidationRules(resultPolicyToSend, numberOfRules) + ar := NewApplicableRules(statusPolicyToSend, latency) + return ar +} diff --git a/agent/pkg/rules/models.go b/agent/pkg/rules/models.go new file mode 100644 index 000000000..8b1f1f617 --- /dev/null +++ b/agent/pkg/rules/models.go @@ -0,0 +1,110 @@ +package rules + +import ( + "encoding/json" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/google/martian/har" + "github.com/up9inc/mizu/shared" + jsonpath "github.com/yalp/jsonpath" +) + +type RulesMatched struct { + Matched bool `json:"matched"` + Rule shared.RulePolicy `json:"rule"` +} + +func appendRulesMatched(rulesMatched []RulesMatched, matched bool, rule shared.RulePolicy) []RulesMatched { + return append(rulesMatched, RulesMatched{Matched: matched, Rule: rule}) +} + +func ValidatePath(URLFromRule string, URL string) bool { + if URLFromRule != "" { + matchPath, err := regexp.MatchString(URLFromRule, URL) + if err != nil || !matchPath { + return false + } + } + return true +} + +func ValidateService(serviceFromRule string, service string) bool { + if serviceFromRule != "" { + matchService, err := regexp.MatchString(serviceFromRule, service) + if err != nil || !matchService { + return false + } + } + return true +} + +func MatchRequestPolicy(harEntry har.Entry, service string) (int, []RulesMatched) { + enforcePolicy, _ := shared.DecodeEnforcePolicy(fmt.Sprintf("%s/%s", shared.RulePolicyPath, shared.RulePolicyFileName)) + var resultPolicyToSend []RulesMatched + for _, rule := range enforcePolicy.Rules { + if !ValidatePath(rule.Path, harEntry.Request.URL) || !ValidateService(rule.Service, service) { + continue + } + if rule.Type == "json" { + var bodyJsonMap interface{} + if err := json.Unmarshal(harEntry.Response.Content.Text, &bodyJsonMap); err != nil { + continue + } + out, err := jsonpath.Read(bodyJsonMap, rule.Key) + if err != nil || out == nil { + continue + } + var matchValue bool + if reflect.TypeOf(out).Kind() == reflect.String { + matchValue, err = regexp.MatchString(rule.Value, out.(string)) + if err != nil { + continue + } + } else { + val := fmt.Sprint(out) + matchValue, err = regexp.MatchString(rule.Value, val) + if err != nil { + continue + } + } + resultPolicyToSend = appendRulesMatched(resultPolicyToSend, matchValue, rule) + } else if rule.Type == "header" { + for j := range harEntry.Response.Headers { + matchKey, err := regexp.MatchString(rule.Key, harEntry.Response.Headers[j].Name) + if err != nil { + continue + } + if matchKey { + matchValue, err := regexp.MatchString(rule.Value, harEntry.Response.Headers[j].Value) + if err != nil { + continue + } + resultPolicyToSend = appendRulesMatched(resultPolicyToSend, matchValue, rule) + } + } + } else { + resultPolicyToSend = appendRulesMatched(resultPolicyToSend, true, rule) + } + } + return len(enforcePolicy.Rules), resultPolicyToSend +} + +func PassedValidationRules(rulesMatched []RulesMatched, numberOfRules int) (bool, int64) { + if len(rulesMatched) == 0 { + return false, 0 + } + for _, rule := range rulesMatched { + if rule.Matched == false { + return false, -1 + } + } + for _, rule := range rulesMatched { + if strings.ToLower(rule.Rule.Type) == "latency" { + return true, rule.Rule.Latency + } + } + return true, -1 +} diff --git a/cli/cmd/tap.go b/cli/cmd/tap.go index 608392f20..098a54824 100644 --- a/cli/cmd/tap.go +++ b/cli/cmd/tap.go @@ -2,12 +2,13 @@ package cmd import ( "errors" + "os" + "github.com/creasty/defaults" "github.com/spf13/cobra" "github.com/up9inc/mizu/cli/mizu" "github.com/up9inc/mizu/cli/mizu/configStructs" "github.com/up9inc/mizu/cli/uiUtils" - "os" ) const analysisMessageToConfirm = `NOTE: running mizu with --analysis flag will upload recorded traffic for further analysis and enriched presentation options.` @@ -64,4 +65,5 @@ func init() { tapCmd.Flags().String(configStructs.HumanMaxEntriesDBSizeTapName, defaultTapConfig.HumanMaxEntriesDBSize, "override the default max entries db size of 200mb") tapCmd.Flags().String(configStructs.DirectionTapName, defaultTapConfig.Direction, "Record traffic that goes in this direction (relative to the tapped pod): in/any") tapCmd.Flags().Bool(configStructs.DryRunTapName, defaultTapConfig.DryRun, "Preview of all pods matching the regex, without tapping them") + tapCmd.Flags().String(configStructs.EnforcePolicyFile, "", "Yaml file with policy rules") } diff --git a/cli/cmd/tapRunner.go b/cli/cmd/tapRunner.go index 949f76f90..f3d8b0228 100644 --- a/cli/cmd/tapRunner.go +++ b/cli/cmd/tapRunner.go @@ -5,15 +5,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/up9inc/mizu/cli/kubernetes" - "github.com/up9inc/mizu/cli/mizu" - "github.com/up9inc/mizu/cli/uiUtils" - "github.com/up9inc/mizu/shared" - "github.com/up9inc/mizu/shared/debounce" - core "k8s.io/api/core/v1" - errors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/tools/clientcmd" "net/http" "net/url" "os" @@ -21,6 +12,17 @@ import ( "regexp" "syscall" "time" + + "github.com/up9inc/mizu/cli/kubernetes" + "github.com/up9inc/mizu/cli/mizu" + "github.com/up9inc/mizu/cli/uiUtils" + "github.com/up9inc/mizu/shared" + "github.com/up9inc/mizu/shared/debounce" + yaml "gopkg.in/yaml.v3" + core "k8s.io/api/core/v1" + errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/clientcmd" ) var mizuServiceAccountExists bool @@ -38,6 +40,14 @@ func RunMizuTap() { if err != nil { return } + var mizuValidationRules string + if mizu.Config.Tap.EnforcePolicyFile != "" { + mizuValidationRules, err = readValidationRules(mizu.Config.Tap.EnforcePolicyFile) + if err != nil { + mizu.Log.Infof("error: %v", err) + return + } + } kubernetesProvider, err := kubernetes.NewProvider(mizu.Config.Tap.KubeConfigPath) if err != nil { @@ -87,7 +97,7 @@ func RunMizuTap() { return } - if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, mizuApiFilteringOptions); err != nil { + if err := createMizuResources(ctx, kubernetesProvider, nodeToTappedPodIPMap, mizuApiFilteringOptions, mizuValidationRules); err != nil { return } @@ -98,7 +108,16 @@ func RunMizuTap() { waitForFinish(ctx, cancel) } -func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, mizuApiFilteringOptions *shared.TrafficFilteringOptions) error { +func readValidationRules(file string) (string, error) { + rules, err := shared.DecodeEnforcePolicy(file) + if err != nil { + return "", err + } + newContent, _ := yaml.Marshal(&rules) + return string(newContent), nil +} + +func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Provider, nodeToTappedPodIPMap map[string][]string, mizuApiFilteringOptions *shared.TrafficFilteringOptions, mizuValidationRules string) error { if err := createMizuNamespace(ctx, kubernetesProvider); err != nil { return err } @@ -111,6 +130,18 @@ func createMizuResources(ctx context.Context, kubernetesProvider *kubernetes.Pro return err } + if err := createMizuConfigmap(ctx, kubernetesProvider, mizuValidationRules); err != nil { + return err + } + + return nil +} + +func createMizuConfigmap(ctx context.Context, kubernetesProvider *kubernetes.Provider, data string) error { + err := kubernetesProvider.ApplyConfigMap(ctx, mizu.ResourcesNamespace, mizu.ConfigMapName, data) + if err != nil { + fmt.Printf("Error creating mizu configmap: %v\n", err) + } return nil } @@ -285,7 +316,6 @@ func watchPodsForTapping(ctx context.Context, kubernetesProvider *kubernetes.Pro mizu.Log.Errorf("Error building node to ips map: %s (%v,%+v)", err, err, err) cancel() } - if err := updateMizuTappers(ctx, kubernetesProvider, nodeToTappedPodIPMap); err != nil { mizu.Log.Errorf("Error updating daemonset: %s (%v,%+v)", err, err, err) cancel() diff --git a/cli/cmd/version.go b/cli/cmd/version.go index 9b0fe8b51..45fc6dc9a 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -1,12 +1,13 @@ package cmd import ( + "strconv" + "time" + "github.com/creasty/defaults" "github.com/spf13/cobra" "github.com/up9inc/mizu/cli/mizu" "github.com/up9inc/mizu/cli/mizu/configStructs" - "strconv" - "time" ) var versionCmd = &cobra.Command{ diff --git a/cli/cmd/viewRunner.go b/cli/cmd/viewRunner.go index e97c2e719..c15c71be7 100644 --- a/cli/cmd/viewRunner.go +++ b/cli/cmd/viewRunner.go @@ -3,11 +3,12 @@ package cmd import ( "context" "fmt" + "net/http" + "github.com/up9inc/mizu/cli/kubernetes" "github.com/up9inc/mizu/cli/mizu" "github.com/up9inc/mizu/cli/uiUtils" "k8s.io/client-go/tools/clientcmd" - "net/http" ) func runMizuView() { diff --git a/cli/go.sum b/cli/go.sum index 1cbdcbeae..ae8e268fd 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -217,6 +217,7 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -410,6 +411,8 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/cli/kubernetes/provider.go b/cli/kubernetes/provider.go index bf4cdc0c9..38f6dc848 100644 --- a/cli/kubernetes/provider.go +++ b/cli/kubernetes/provider.go @@ -6,15 +6,16 @@ import ( "encoding/json" "errors" "fmt" - "github.com/up9inc/mizu/cli/mizu" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/util/homedir" "os" "path/filepath" "regexp" "strconv" + "github.com/up9inc/mizu/cli/mizu" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/homedir" + "github.com/up9inc/mizu/shared" core "k8s.io/api/core/v1" rbac "k8s.io/api/rbac/v1" @@ -130,6 +131,10 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, namespace if err != nil { return nil, err } + configMapVolumeName := &core.ConfigMapVolumeSource{} + configMapVolumeName.Name = mizu.ConfigMapName + configMapOptional := true + configMapVolumeName.Optional = &configMapOptional cpuLimit, err := resource.ParseQuantity("750m") if err != nil { @@ -160,7 +165,13 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, namespace Name: podName, Image: podImage, ImagePullPolicy: core.PullAlways, - Command: []string{"./mizuagent", "--api-server"}, + VolumeMounts: []core.VolumeMount{ + { + Name: mizu.ConfigMapName, + MountPath: shared.RulePolicyPath, + }, + }, + Command: []string{"./mizuagent", "--api-server"}, Env: []core.EnvVar{ { Name: shared.HostModeEnvVar, @@ -187,6 +198,14 @@ func (provider *Provider) CreateMizuApiServerPod(ctx context.Context, namespace }, }, }, + Volumes: []core.Volume{ + { + Name: mizu.ConfigMapName, + VolumeSource: core.VolumeSource{ + ConfigMap: configMapVolumeName, + }, + }, + }, DNSPolicy: core.DNSClusterFirstWithHostNet, TerminationGracePeriodSeconds: new(int64), }, @@ -370,6 +389,15 @@ func (provider *Provider) RemoveDaemonSet(ctx context.Context, namespace string, return provider.clientSet.AppsV1().DaemonSets(namespace).Delete(ctx, daemonSetName, metav1.DeleteOptions{}) } +func (provider *Provider) RemoveConfigMap(ctx context.Context, namespace string, configMapName string) error { + if isFound, err := provider.CheckConfigMapExists(ctx, namespace, configMapName); err != nil { + return err + } else if !isFound { + return nil + } + return provider.clientSet.CoreV1().ConfigMaps(namespace).Delete(ctx, configMapName, metav1.DeleteOptions{}) +} + func (provider *Provider) CheckNamespaceExists(ctx context.Context, name string) (bool, error) { listOptions := metav1.ListOptions{ FieldSelector: fmt.Sprintf("metadata.name=%s", name), @@ -472,6 +500,50 @@ func (provider *Provider) CheckDaemonSetExists(ctx context.Context, namespace st return false, nil } +func (provider *Provider) CheckConfigMapExists(ctx context.Context, namespace string, name string) (bool, error) { + listOptions := metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", name), + Limit: 1, + } + resourceList, err := provider.clientSet.CoreV1().ConfigMaps(namespace).List(ctx, listOptions) + if err != nil { + return false, err + } + + if len(resourceList.Items) > 0 { + return true, nil + } + + return false, nil +} + +func (provider *Provider) ApplyConfigMap(ctx context.Context, namespace string, configMapName string, data string) error { + if data == "" { + return nil + } + configMapData := make(map[string]string, 0) + configMapData[shared.RulePolicyFileName] = data + configMap := &core.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: configMapData, + } + _, err := provider.clientSet.CoreV1().ConfigMaps(namespace).Create(ctx, configMap, metav1.CreateOptions{}) + var statusError *k8serrors.StatusError + if errors.As(err, &statusError) { + if statusError.ErrStatus.Reason == metav1.StatusReasonForbidden { + return fmt.Errorf("User not authorized to create configmap, --test-rules will be ignored") + } + } + return err +} + func (provider *Provider) ApplyMizuTapperDaemonSet(ctx context.Context, namespace string, daemonSetName string, podImage string, tapperPodName string, apiServerPodIp string, nodeToTappedPodIPMap map[string][]string, serviceAccountName string, tapOutgoing bool) error { mizu.Log.Debugf("Applying %d tapper deamonsets, ns: %s, daemonSetName: %s, podImage: %s, tapperPodName: %s", len(nodeToTappedPodIPMap), namespace, daemonSetName, podImage, tapperPodName) diff --git a/cli/mizu/config.go b/cli/mizu/config.go index 33293812d..38c342130 100644 --- a/cli/mizu/config.go +++ b/cli/mizu/config.go @@ -3,17 +3,18 @@ package mizu import ( "errors" "fmt" - "github.com/creasty/defaults" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/up9inc/mizu/cli/uiUtils" - "gopkg.in/yaml.v3" "io/ioutil" "os" "path" "reflect" "strconv" "strings" + + "github.com/creasty/defaults" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/up9inc/mizu/cli/uiUtils" + "gopkg.in/yaml.v3" ) const ( diff --git a/cli/mizu/configStruct.go b/cli/mizu/configStruct.go index 2a08f44aa..d3c1d5864 100644 --- a/cli/mizu/configStruct.go +++ b/cli/mizu/configStruct.go @@ -2,6 +2,7 @@ package mizu import ( "fmt" + "github.com/up9inc/mizu/cli/mizu/configStructs" ) diff --git a/cli/mizu/configStructs/tapConfig.go b/cli/mizu/configStructs/tapConfig.go index ee4fda63f..d00d5097a 100644 --- a/cli/mizu/configStructs/tapConfig.go +++ b/cli/mizu/configStructs/tapConfig.go @@ -3,9 +3,10 @@ package configStructs import ( "errors" "fmt" - "github.com/up9inc/mizu/shared/units" "regexp" "strings" + + "github.com/up9inc/mizu/shared/units" ) const ( @@ -20,6 +21,7 @@ const ( HumanMaxEntriesDBSizeTapName = "max-entries-db-size" DirectionTapName = "direction" DryRunTapName = "dry-run" + EnforcePolicyFile = "test-rules" ) type TapConfig struct { @@ -37,6 +39,7 @@ type TapConfig struct { HumanMaxEntriesDBSize string `yaml:"max-entries-db-size" default:"200MB"` Direction string `yaml:"direction" default:"in"` DryRun bool `yaml:"dry-run" default:"false"` + EnforcePolicyFile string `yaml:"test-rules"` } func (config *TapConfig) PodRegex() *regexp.Regexp { diff --git a/cli/mizu/consts.go b/cli/mizu/consts.go index 1924f8c5c..f9ade7e5a 100644 --- a/cli/mizu/consts.go +++ b/cli/mizu/consts.go @@ -22,6 +22,7 @@ const ( ServiceAccountName = "mizu-service-account" TapperDaemonSetName = "mizu-tapper-daemon-set" TapperPodName = "mizu-tapper" + ConfigMapName = "mizu-policy" ) func getMizuFolderPath() string { diff --git a/cli/mizu/versionCheck.go b/cli/mizu/versionCheck.go index b61133826..d4a791629 100644 --- a/cli/mizu/versionCheck.go +++ b/cli/mizu/versionCheck.go @@ -4,14 +4,15 @@ import ( "context" "encoding/json" "fmt" - "github.com/google/go-github/v37/github" - "github.com/up9inc/mizu/cli/uiUtils" - "github.com/up9inc/mizu/shared" - "github.com/up9inc/mizu/shared/semver" "io/ioutil" "net/http" "net/url" "time" + + "github.com/google/go-github/v37/github" + "github.com/up9inc/mizu/cli/uiUtils" + "github.com/up9inc/mizu/shared" + "github.com/up9inc/mizu/shared/semver" ) func getApiVersion(port uint16) (string, error) { diff --git a/shared/consts.go b/shared/consts.go index 7effd97fa..71cafdff2 100644 --- a/shared/consts.go +++ b/shared/consts.go @@ -6,4 +6,6 @@ const ( NodeNameEnvVar = "NODE_NAME" TappedAddressesPerNodeDictEnvVar = "TAPPED_ADDRESSES_PER_HOST" MaxEntriesDBSizeBytesEnvVar = "MAX_ENTRIES_DB_BYTES" + RulePolicyPath = "/app/enforce-policy/" + RulePolicyFileName = "enforce-policy.yaml" ) diff --git a/shared/go.mod b/shared/go.mod index 66e5165d6..157d3e5fa 100644 --- a/shared/go.mod +++ b/shared/go.mod @@ -3,7 +3,8 @@ module github.com/up9inc/mizu/shared go 1.16 require ( + github.com/google/martian v2.1.0+incompatible // indirect github.com/gorilla/websocket v1.4.2 + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect github.com/docker/go-units v0.4.0 ) - diff --git a/shared/go.sum b/shared/go.sum index b46c3a514..498bce1d3 100644 --- a/shared/go.sum +++ b/shared/go.sum @@ -1,4 +1,8 @@ +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= diff --git a/shared/models.go b/shared/models.go index 1363d8316..2a70fa212 100644 --- a/shared/models.go +++ b/shared/models.go @@ -1,5 +1,13 @@ package shared +import ( + "fmt" + "io/ioutil" + "strings" + + yaml "gopkg.in/yaml.v3" +) + type WebSocketMessageType string const ( @@ -32,7 +40,7 @@ type WebSocketStatusMessage struct { } type TapStatus struct { - Pods []PodInfo `json:"pods"` + Pods []PodInfo `json:"pods"` TLSLinks []TLSLinkInfo `json:"tlsLinks"` } @@ -76,3 +84,75 @@ type VersionResponse struct { SemVer string `json:"semver"` } +type RulesPolicy struct { + Rules []RulePolicy `yaml:"rules"` +} + +type RulePolicy struct { + Type string `yaml:"type"` + Service string `yaml:"service"` + Path string `yaml:"path"` + Method string `yaml:"method"` + Key string `yaml:"key"` + Value string `yaml:"value"` + Latency int64 `yaml:"latency"` + Name string `yaml:"name"` +} + +func (r *RulePolicy) validateType() bool { + permitedTypes := []string{"json", "header", "latency"} + _, found := Find(permitedTypes, r.Type) + if !found { + fmt.Printf("\nRule with name %s will be ignored. Err: only json, header and latency types are supported on rule definition.\n", r.Name) + } + if strings.ToLower(r.Type) == "latency" { + if r.Latency == 0 { + fmt.Printf("\nRule with name %s will be ignored. Err: when type=latency, the field Latency should be specified and have a value >= 1\n\n", r.Name) + found = false + } + } + return found +} + +func (rules *RulesPolicy) ValidateRulesPolicy() []int { + invalidIndex := make([]int, 0) + for i := range rules.Rules { + validated := rules.Rules[i].validateType() + if !validated { + invalidIndex = append(invalidIndex, i) + } + } + return invalidIndex +} + +func (rules *RulesPolicy) RemoveRule(idx int) { + rules.Rules = append(rules.Rules[:idx], rules.Rules[idx+1:]...) +} + +func Find(slice []string, val string) (int, bool) { + for i, item := range slice { + if item == val { + return i, true + } + } + return -1, false +} + +func DecodeEnforcePolicy(path string) (RulesPolicy, error) { + content, err := ioutil.ReadFile(path) + enforcePolicy := RulesPolicy{} + if err != nil { + return enforcePolicy, err + } + err = yaml.Unmarshal([]byte(content), &enforcePolicy) + if err != nil { + return enforcePolicy, err + } + invalidIndex := enforcePolicy.ValidateRulesPolicy() + if len(invalidIndex) != 0 { + for i := range invalidIndex { + enforcePolicy.RemoveRule(invalidIndex[i]) + } + } + return enforcePolicy, nil +} diff --git a/tap/har_writer.go b/tap/har_writer.go index 25ff4f5f9..a20dbfb99 100644 --- a/tap/har_writer.go +++ b/tap/har_writer.go @@ -43,7 +43,7 @@ func openNewHarFile(filename string) *HarFile { } type HarFile struct { - file *os.File + file *os.File entryCount int } @@ -105,13 +105,13 @@ func NewEntry(request *http.Request, requestTime time.Time, response *http.Respo harEntry := har.Entry{ StartedDateTime: time.Now().UTC(), - Time: totalTime, - Request: harRequest, - Response: harResponse, - Cache: &har.Cache{}, + Time: totalTime, + Request: harRequest, + Response: harResponse, + Cache: &har.Cache{}, Timings: &har.Timings{ - Send: -1, - Wait: -1, + Send: -1, + Wait: -1, Receive: totalTime, }, } @@ -155,14 +155,14 @@ func (f *HarFile) Close() { } } -func (f*HarFile) writeHeader() { +func (f *HarFile) writeHeader() { header := []byte(`{"log": {"version": "1.2", "creator": {"name": "Mizu", "version": "0.0.1"}, "entries": [`) if _, err := f.file.Write(header); err != nil { log.Panicf("Failed to write header to output file: %s (%v,%+v)", err, err, err) } } -func (f*HarFile) writeTrailer() { +func (f *HarFile) writeTrailer() { trailer := []byte("]}}") if _, err := f.file.Write(trailer); err != nil { log.Panicf("Failed to write trailer to output file: %s (%v,%+v)", err, err, err) @@ -172,26 +172,27 @@ func (f*HarFile) writeTrailer() { func NewHarWriter(outputDir string, maxEntries int) *HarWriter { return &HarWriter{ OutputDirPath: outputDir, - MaxEntries: maxEntries, - PairChan: make(chan *PairChanItem), - OutChan: make(chan *OutputChannelItem, 1000), - currentFile: nil, - done: make(chan bool), + MaxEntries: maxEntries, + PairChan: make(chan *PairChanItem), + OutChan: make(chan *OutputChannelItem, 1000), + currentFile: nil, + done: make(chan bool), } } type OutputChannelItem struct { - HarEntry *har.Entry - ConnectionInfo *ConnectionInfo + HarEntry *har.Entry + ConnectionInfo *ConnectionInfo + ValidationRulesChecker string } type HarWriter struct { OutputDirPath string - MaxEntries int - PairChan chan *PairChanItem - OutChan chan *OutputChannelItem - currentFile *HarFile - done chan bool + MaxEntries int + PairChan chan *PairChanItem + OutChan chan *OutputChannelItem + currentFile *HarFile + done chan bool } func (hw *HarWriter) WritePair(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time, connectionInfo *ConnectionInfo) { @@ -240,7 +241,7 @@ func (hw *HarWriter) Start() { hw.closeFile() } hw.done <- true - } () + }() } func (hw *HarWriter) Stop() { diff --git a/ui/package-lock.json b/ui/package-lock.json index b57c27a3d..9ce39e95f 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -32814,6 +32814,23 @@ "universalify": "^2.0.0" } }, + "jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "requires": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + }, + "dependencies": { + "esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=" + } + } + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -37761,6 +37778,69 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==" }, + "static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "requires": { + "escodegen": "^1.8.1" + }, + "dependencies": { + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + } + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -38647,6 +38727,11 @@ "which-boxed-primitive": "^1.0.2" } }, + "underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, "unicode-canonical-property-names-ecmascript": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", diff --git a/ui/package.json b/ui/package.json index 459e0a25a..b21551068 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "@types/node": "^12.20.10", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", + "jsonpath": "^1.1.1", "axios": "^0.21.1", "node-sass": "^5.0.0", "numeral": "^2.0.6", diff --git a/ui/src/components/HarEntry.tsx b/ui/src/components/HarEntry.tsx index 368ac92cb..f51a0bcf0 100644 --- a/ui/src/components/HarEntry.tsx +++ b/ui/src/components/HarEntry.tsx @@ -19,6 +19,13 @@ interface HAREntry { isCurrentRevision?: boolean; timestamp: Date; isOutgoing?: boolean; + latency: number; + rules: Rules; +} + +interface Rules { + status: boolean; + latency: number } interface HAREntryProps { @@ -48,9 +55,16 @@ export const HarEntry: React.FC = ({entry, setFocusedEntryId, isS break; } } - + let backgroundColor = ""; + if ('latency' in entry.rules) { + if (entry.rules.latency !== -1) { + backgroundColor = entry.rules.latency >= entry.latency ? styles.ruleSuccessRow : styles.ruleFailureRow + } else { + backgroundColor = entry.rules.status ? styles.ruleSuccessRow : styles.ruleFailureRow + } + } return <> -
setFocusedEntryId(entry.id)}> +
setFocusedEntryId(entry.id)}> {entry.statusCode &&
} diff --git a/ui/src/components/HarEntryDetailed.tsx b/ui/src/components/HarEntryDetailed.tsx index c8ee78f3b..8e86d140f 100644 --- a/ui/src/components/HarEntryDetailed.tsx +++ b/ui/src/components/HarEntryDetailed.tsx @@ -29,7 +29,7 @@ const HarEntryTitle: React.FC = ({har}) => { const classes = useStyles(); const {log: {entries}} = har; - const {response, request, timings: {receive}} = entries[0]; + const {response, request, timings: {receive}} = entries[0].entry; const {status, statusText, bodySize} = response; @@ -40,9 +40,10 @@ const HarEntryTitle: React.FC = ({har}) => {
-
{formatSize(bodySize)}
-
{status} {statusText}
-
{Math.round(receive)}ms
+
{formatSize(bodySize)}
+
{status} {statusText}
+
{Math.round(receive)}ms
+
{'rulesMatched' in entries[0] ? entries[0].rulesMatched?.length : '0'} Rules Applied
; }; diff --git a/ui/src/components/HarEntryViewer/HAREntrySections.module.sass b/ui/src/components/HarEntryViewer/HAREntrySections.module.sass index 8e56935cf..50bb0cc19 100644 --- a/ui/src/components/HarEntryViewer/HAREntrySections.module.sass +++ b/ui/src/components/HarEntryViewer/HAREntrySections.module.sass @@ -40,6 +40,27 @@ width: 1% max-width: 15rem + .rulesTitleSuccess + color: #0C0B1A + + .rulesMatchedSuccess + background: #E8FFF1 + padding: 5px + border-radius: 4px + color: #219653 + font-style: normal + font-size: 0.7rem + font-weight: 600 + + .rulesMatchedFailure + background: #FFE9EF + padding: 5px + border-radius: 4px + color: #DB2156 + font-style: normal + font-size: 0.7rem + font-weight: 600 + .dataValue color: $blue-gray margin: 0 @@ -66,7 +87,6 @@ border-top: 1px solid $light-blue-color padding: 1rem background: none - table width: 100% tr td:first-child diff --git a/ui/src/components/HarEntryViewer/HAREntrySections.tsx b/ui/src/components/HarEntryViewer/HAREntrySections.tsx index c73c08bb0..088dd3f51 100644 --- a/ui/src/components/HarEntryViewer/HAREntrySections.tsx +++ b/ui/src/components/HarEntryViewer/HAREntrySections.tsx @@ -5,6 +5,7 @@ import CollapsibleContainer from "../CollapsibleContainer"; import FancyTextDisplay from "../FancyTextDisplay"; import Checkbox from "../Checkbox"; import ProtobufDecoder from "protobuf-decoder"; +var jp = require('jsonpath'); interface HAREntryViewLineProps { label: string; @@ -144,3 +145,122 @@ export const HAREntryTableSection: React.FC = ({title, arr } } + + + +interface HAREntryPolicySectionProps { + service: string, + title: string, + response: any, + latency?: number, + arrayToIterate: any[], +} + + +interface HAREntryPolicySectionCollapsibleTitleProps { + label: string; + matched: string; + isExpanded: boolean; +} + +const HAREntryPolicySectionCollapsibleTitle: React.FC = ({label, matched, isExpanded}) => { + return
+ + {isExpanded ? '-' : '+'} + + + + {label} + {matched} + + +
+} + +interface HAREntryPolicySectionContainerProps { + label: string; + matched: string; + children?: any; +} + +export const HAREntryPolicySectionContainer: React.FC = ({label, matched, children}) => { + const [expanded, setExpanded] = useState(false); + return setExpanded(!expanded)} + title={} + > + {children} + +} + +export const HAREntryTablePolicySection: React.FC = ({service, title, response, latency, arrayToIterate}) => { + const base64ToJson = response.content.mimeType === "application/json; charset=utf-8" ? JSON.parse(Buffer.from(response.content.text, "base64").toString()) : {}; + return + { + arrayToIterate && arrayToIterate.length > 0 ? + <> + + + + {arrayToIterate.map(({rule, matched}, index) => { + + + return ( + = latency : true)? "Success" : "Failure"}> + { + + <> + { + rule.Key != "" ? + + : null + } + { + rule.Latency != "" ? + + : null + } + { + rule.Method != "" ? + + : null + } + { + rule.Path != "" ? + + : null + } + { + rule.Service != "" ? + + : null + } + { + rule.Type != "" ? + + : null + } + { + rule.Value != "" ? + + : null + } + + } + + + + ) + } + ) + } + +
Key:{rule.Key}
Latency: {rule.Latency}
Method: {rule.Method}
Path: {rule.Path}
Service: {service}
Type: {rule.Type}
Value: {rule.Value}
+
+ + : + } +
+} \ No newline at end of file diff --git a/ui/src/components/HarEntryViewer/HAREntryViewer.tsx b/ui/src/components/HarEntryViewer/HAREntryViewer.tsx index 801d5cb2d..e0450e1e7 100644 --- a/ui/src/components/HarEntryViewer/HAREntryViewer.tsx +++ b/ui/src/components/HarEntryViewer/HAREntryViewer.tsx @@ -1,19 +1,22 @@ import React, {useState} from 'react'; import styles from './HAREntryViewer.module.sass'; import Tabs from "../Tabs"; -import {HAREntryTableSection, HAREntryBodySection} from "./HAREntrySections"; +import {HAREntryTableSection, HAREntryBodySection, HAREntryTablePolicySection} from "./HAREntrySections"; const MIME_TYPE_KEY = 'mimeType'; -const HAREntryDisplay: React.FC = ({entry, isCollapsed: initialIsCollapsed, isResponseMocked}) => { - const {request, response} = entry; - +const HAREntryDisplay: React.FC = ({har, entry, isCollapsed: initialIsCollapsed, isResponseMocked}) => { + const {request, response, timings: {receive}} = entry; + const rulesMatched = har.log.entries[0].rulesMatched const TABS = [ {tab: 'request'}, { tab: 'response', badge: <>{isResponseMocked && MOCK} }, + { + tab: 'Rules', + }, ]; const [currentTab, setCurrentTab] = useState(TABS[0].tab); @@ -43,6 +46,9 @@ const HAREntryDisplay: React.FC = ({entry, isCollapsed: initialIsCollapsed, } + {currentTab === TABS[2].tab && + + }
} ; } @@ -58,7 +64,7 @@ const HAREntryViewer: React.FC = ({harObject, className, isResponseMocked const {log: {entries}} = harObject; const isCollapsed = entries.length > 1; return
- {Object.keys(entries).map((entry: any, index) => )} + {Object.keys(entries).map((entry: any, index) => )}
}; diff --git a/ui/src/components/HarPage.tsx b/ui/src/components/HarPage.tsx index b96404cba..bfa7dbfb3 100644 --- a/ui/src/components/HarPage.tsx +++ b/ui/src/components/HarPage.tsx @@ -72,7 +72,6 @@ export const HarPage: React.FC = ({setAnalyzeStatus, onTLSDetected ws.current.onmessage = e => { if (!e?.data) return; const message = JSON.parse(e.data); - switch (message.messageType) { case "entry": const entry = message.data diff --git a/ui/src/components/style/HarEntry.module.sass b/ui/src/components/style/HarEntry.module.sass index fc5442f48..5425b1786 100644 --- a/ui/src/components/style/HarEntry.module.sass +++ b/ui/src/components/style/HarEntry.module.sass @@ -23,6 +23,14 @@ margin-left: 10px margin-right: 3px +.ruleSuccessRow + border: 1px $success-color solid + border-left: 5px $success-color solid + +.ruleFailureRow + border: 1px $failure-color solid + border-left: 5px $failure-color solid + .service text-overflow: ellipsis overflow: hidden