mirror of
https://github.com/kubeshark/kubeshark.git
synced 2025-06-25 15:54:43 +00:00
Service Map (#623)
* debug builds and gcflags * update dockerfile for debug * service map routes and controller * service map graph structure * service map interface and new methods * adding service map edges from mizu entries * new service map count methods * implementing the status endpoint * ServiceMapResponse and ServiceMapEdge models * service map get endpoint logic * reset logic and endpoint * fixed service map get status * improvements to graph node structure * front-end implementation and service map buttons * new render endpoint to render the graph in real time * spinner sass * new ServiceMapModal component * testing react-force-graph-2d lib * Improvements to service map graph structure, added node id and updated edge source/destination type * Revert "testing react-force-graph-2d lib" This reverts commit1153938386
. * testing react-graph-vis lib * updated to work with react-graph-vis lib * removed render endpoint * go mod tidy * serviceMap config flag * using the serviceMap config flag * passing mizu config to service map as a dependency * service map tests * Removed print functions * finished service map tests * new service property * service map controller tests * moved service map reset button to service map modal reset closes the modal * service map modal refresh button and logic * reset button resets data and refresh * service map modal close button * node size/edge size based on the count value edge color based on protocol * nodes and edges shadow * enabled physics to avoid node overlap, changed kafka protocol color to dark green * showing edges count values and fixed bidirectional edges overlap * go mod tidy * removed console.log * Using the destination node protocol instead of the source node protocol * Revert "debug builds and gcflags" Addressed by #624 and #626 This reverts commit17ecaece3e
. * Revert "update dockerfile for debug" Addressed by #635 This reverts commit5dfc15b140
. * using the entire tap Protocol struct instead of only the protocol name * using the backend protocol background color for node colors * fixed test, the node list order can change * re-factoring to get 100% coverage * using protocol colors just for edges * re-factored service map to use TCP Entry data. Node key is the entry ip-address instead of the name * fallback to ip-address when entry name is unresolved * re-factored front-end * adjustment to main div style * added support for multiple protocols for the same edge * using the item protocol instead of the extension variable * fixed controller tests * displaying service name and ip-address on graph nodes * fixed service map test, we cannot guarantee the slice order * auth middleware * created a new pkg for the service map * re-factoring * re-factored front-end * reverting the import order as previous * Aligning with other UI feature flags handling * we don't need to get the status anymore, we have window["isServiceMapEnabled"] * small adjustments * renamed from .tsx to .ts * button styles and minor improvements * moved service map modal from trafficPage to app component Co-authored-by: Igor Gov <igor.govorov1@gmail.com>
This commit is contained in:
parent
7477f867f9
commit
d5fd2ff1da
@ -19,6 +19,7 @@ require (
|
||||
github.com/orcaman/concurrent-map v0.0.0-20210106121528-16402b402231
|
||||
github.com/ory/kratos-client-go v0.8.2-alpha.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/up9inc/basenine/client/go v0.0.0-20220110083745-04fbc6c2068d
|
||||
github.com/up9inc/mizu/shared v0.0.0
|
||||
github.com/up9inc/mizu/tap v0.0.0
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"mizuserver/pkg/models"
|
||||
"mizuserver/pkg/oas"
|
||||
"mizuserver/pkg/routes"
|
||||
"mizuserver/pkg/servicemap"
|
||||
"mizuserver/pkg/up9"
|
||||
"mizuserver/pkg/utils"
|
||||
"net/http"
|
||||
@ -153,6 +154,9 @@ func enableExpFeatureIfNeeded() {
|
||||
if config.Config.OAS {
|
||||
oas.GetOasGeneratorInstance().Start()
|
||||
}
|
||||
if config.Config.ServiceMap {
|
||||
servicemap.GetInstance().SetConfig(config.Config)
|
||||
}
|
||||
}
|
||||
|
||||
func configureBasenineServer(host string, port string) {
|
||||
@ -254,10 +258,12 @@ func hostApi(socketHarOutputChannel chan<- *tapApi.OutputChannelItem) {
|
||||
routes.UserRoutes(app)
|
||||
routes.InstallRoutes(app)
|
||||
}
|
||||
|
||||
if config.Config.OAS {
|
||||
routes.OASRoutes(app)
|
||||
}
|
||||
if config.Config.ServiceMap {
|
||||
routes.ServiceMapRoutes(app)
|
||||
}
|
||||
|
||||
routes.QueryRoutes(app)
|
||||
routes.EntriesRoutes(app)
|
||||
@ -286,6 +292,7 @@ func setUIFlags() error {
|
||||
|
||||
replacedContent := strings.Replace(string(read), "__IS_STANDALONE__", strconv.FormatBool(config.Config.StandaloneMode), 1)
|
||||
replacedContent = strings.Replace(replacedContent, "__IS_OAS_ENABLED__", strconv.FormatBool(config.Config.OAS), 1)
|
||||
replacedContent = strings.Replace(replacedContent, "__IS_SERVICE_MAP_ENABLED__", strconv.FormatBool(config.Config.ServiceMap), 1)
|
||||
|
||||
err = ioutil.WriteFile(uiIndexPath, []byte(replacedContent), 0)
|
||||
if err != nil {
|
||||
|
@ -13,6 +13,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mizuserver/pkg/servicemap"
|
||||
|
||||
"github.com/google/martian/har"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
@ -146,6 +148,8 @@ func startReadingChannel(outputItems <-chan *tapApi.OutputChannelItem, extension
|
||||
panic(err)
|
||||
}
|
||||
connection.SendText(string(data))
|
||||
|
||||
servicemap.GetInstance().NewTCPEntry(mizuEntry.Source, mizuEntry.Destination, &item.Protocol)
|
||||
}
|
||||
}
|
||||
|
||||
|
36
agent/pkg/controllers/service_map_controller.go
Normal file
36
agent/pkg/controllers/service_map_controller.go
Normal file
@ -0,0 +1,36 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"mizuserver/pkg/servicemap"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ServiceMapController struct {
|
||||
service servicemap.ServiceMap
|
||||
}
|
||||
|
||||
func NewServiceMapController() *ServiceMapController {
|
||||
return &ServiceMapController{
|
||||
service: servicemap.GetInstance(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServiceMapController) Status(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, s.service.GetStatus())
|
||||
}
|
||||
|
||||
func (s *ServiceMapController) Get(c *gin.Context) {
|
||||
response := &servicemap.ServiceMapResponse{
|
||||
Status: s.service.GetStatus(),
|
||||
Nodes: s.service.GetNodes(),
|
||||
Edges: s.service.GetEdges(),
|
||||
}
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (s *ServiceMapController) Reset(c *gin.Context) {
|
||||
s.service.Reset()
|
||||
s.Status(c)
|
||||
}
|
147
agent/pkg/controllers/service_map_controller_test.go
Normal file
147
agent/pkg/controllers/service_map_controller_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"mizuserver/pkg/servicemap"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
tapApi "github.com/up9inc/mizu/tap/api"
|
||||
)
|
||||
|
||||
const (
|
||||
a = "aService"
|
||||
b = "bService"
|
||||
Ip = "127.0.0.1"
|
||||
Port = "80"
|
||||
)
|
||||
|
||||
var (
|
||||
TCPEntryA = &tapApi.TCP{
|
||||
Name: a,
|
||||
Port: Port,
|
||||
IP: fmt.Sprintf("%s.%s", Ip, a),
|
||||
}
|
||||
TCPEntryB = &tapApi.TCP{
|
||||
Name: b,
|
||||
Port: Port,
|
||||
IP: fmt.Sprintf("%s.%s", Ip, b),
|
||||
}
|
||||
)
|
||||
|
||||
var ProtocolHttp = &tapApi.Protocol{
|
||||
Name: "http",
|
||||
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
|
||||
Abbreviation: "HTTP",
|
||||
Macro: "http",
|
||||
Version: "1.1",
|
||||
BackgroundColor: "#205cf5",
|
||||
ForegroundColor: "#ffffff",
|
||||
FontSize: 12,
|
||||
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
|
||||
Ports: []string{"80", "443", "8080"},
|
||||
Priority: 0,
|
||||
}
|
||||
|
||||
type ServiceMapControllerSuite struct {
|
||||
suite.Suite
|
||||
|
||||
c *ServiceMapController
|
||||
w *httptest.ResponseRecorder
|
||||
g *gin.Context
|
||||
}
|
||||
|
||||
func (s *ServiceMapControllerSuite) SetupTest() {
|
||||
s.c = NewServiceMapController()
|
||||
s.c.service.SetConfig(&shared.MizuAgentConfig{
|
||||
ServiceMap: true,
|
||||
})
|
||||
s.c.service.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||
|
||||
s.w = httptest.NewRecorder()
|
||||
s.g, _ = gin.CreateTestContext(s.w)
|
||||
}
|
||||
|
||||
func (s *ServiceMapControllerSuite) TestGetStatus() {
|
||||
assert := s.Assert()
|
||||
|
||||
s.c.Status(s.g)
|
||||
assert.Equal(http.StatusOK, s.w.Code)
|
||||
|
||||
var status servicemap.ServiceMapStatus
|
||||
err := json.Unmarshal(s.w.Body.Bytes(), &status)
|
||||
assert.NoError(err)
|
||||
assert.Equal("enabled", status.Status)
|
||||
assert.Equal(1, status.EntriesProcessedCount)
|
||||
assert.Equal(2, status.NodeCount)
|
||||
assert.Equal(1, status.EdgeCount)
|
||||
}
|
||||
|
||||
func (s *ServiceMapControllerSuite) TestGet() {
|
||||
assert := s.Assert()
|
||||
|
||||
s.c.Get(s.g)
|
||||
assert.Equal(http.StatusOK, s.w.Code)
|
||||
|
||||
var response servicemap.ServiceMapResponse
|
||||
err := json.Unmarshal(s.w.Body.Bytes(), &response)
|
||||
assert.NoError(err)
|
||||
|
||||
// response status
|
||||
assert.Equal("enabled", response.Status.Status)
|
||||
assert.Equal(1, response.Status.EntriesProcessedCount)
|
||||
assert.Equal(2, response.Status.NodeCount)
|
||||
assert.Equal(1, response.Status.EdgeCount)
|
||||
|
||||
// response nodes
|
||||
aNode := servicemap.ServiceMapNode{
|
||||
Id: 1,
|
||||
Name: TCPEntryA.IP,
|
||||
Entry: TCPEntryA,
|
||||
Count: 1,
|
||||
}
|
||||
bNode := servicemap.ServiceMapNode{
|
||||
Id: 2,
|
||||
Name: TCPEntryB.IP,
|
||||
Entry: TCPEntryB,
|
||||
Count: 1,
|
||||
}
|
||||
assert.Contains(response.Nodes, aNode)
|
||||
assert.Contains(response.Nodes, bNode)
|
||||
assert.Len(response.Nodes, 2)
|
||||
|
||||
// response edges
|
||||
assert.Equal([]servicemap.ServiceMapEdge{
|
||||
{
|
||||
Source: aNode,
|
||||
Destination: bNode,
|
||||
Protocol: ProtocolHttp,
|
||||
Count: 1,
|
||||
},
|
||||
}, response.Edges)
|
||||
}
|
||||
|
||||
func (s *ServiceMapControllerSuite) TestGetReset() {
|
||||
assert := s.Assert()
|
||||
|
||||
s.c.Reset(s.g)
|
||||
assert.Equal(http.StatusOK, s.w.Code)
|
||||
|
||||
var status servicemap.ServiceMapStatus
|
||||
err := json.Unmarshal(s.w.Body.Bytes(), &status)
|
||||
assert.NoError(err)
|
||||
assert.Equal("enabled", status.Status)
|
||||
assert.Equal(0, status.EntriesProcessedCount)
|
||||
assert.Equal(0, status.NodeCount)
|
||||
assert.Equal(0, status.EdgeCount)
|
||||
}
|
||||
|
||||
func TestServiceMapControllerSuite(t *testing.T) {
|
||||
suite.Run(t, new(ServiceMapControllerSuite))
|
||||
}
|
19
agent/pkg/routes/service_map_routes.go
Normal file
19
agent/pkg/routes/service_map_routes.go
Normal file
@ -0,0 +1,19 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"mizuserver/pkg/controllers"
|
||||
"mizuserver/pkg/middlewares"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ServiceMapRoutes(ginApp *gin.Engine) {
|
||||
routeGroup := ginApp.Group("/servicemap")
|
||||
routeGroup.Use(middlewares.RequiresAuth())
|
||||
|
||||
controller := controllers.NewServiceMapController()
|
||||
|
||||
routeGroup.GET("/status", controller.Status)
|
||||
routeGroup.GET("/get", controller.Get)
|
||||
routeGroup.GET("/reset", controller.Reset)
|
||||
}
|
32
agent/pkg/servicemap/models.go
Normal file
32
agent/pkg/servicemap/models.go
Normal file
@ -0,0 +1,32 @@
|
||||
package servicemap
|
||||
|
||||
import (
|
||||
tapApi "github.com/up9inc/mizu/tap/api"
|
||||
)
|
||||
|
||||
type ServiceMapStatus struct {
|
||||
Status string `json:"status"`
|
||||
EntriesProcessedCount int `json:"entriesProcessedCount"`
|
||||
NodeCount int `json:"nodeCount"`
|
||||
EdgeCount int `json:"edgeCount"`
|
||||
}
|
||||
|
||||
type ServiceMapResponse struct {
|
||||
Status ServiceMapStatus `json:"status"`
|
||||
Nodes []ServiceMapNode `json:"nodes"`
|
||||
Edges []ServiceMapEdge `json:"edges"`
|
||||
}
|
||||
|
||||
type ServiceMapNode struct {
|
||||
Id int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Entry *tapApi.TCP `json:"entry"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type ServiceMapEdge struct {
|
||||
Source ServiceMapNode `json:"source"`
|
||||
Destination ServiceMapNode `json:"destination"`
|
||||
Count int `json:"count"`
|
||||
Protocol *tapApi.Protocol `json:"protocol"`
|
||||
}
|
271
agent/pkg/servicemap/servicemap.go
Normal file
271
agent/pkg/servicemap/servicemap.go
Normal file
@ -0,0 +1,271 @@
|
||||
package servicemap
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/up9inc/mizu/shared"
|
||||
"github.com/up9inc/mizu/shared/logger"
|
||||
tapApi "github.com/up9inc/mizu/tap/api"
|
||||
)
|
||||
|
||||
const (
|
||||
ServiceMapEnabled = "enabled"
|
||||
ServiceMapDisabled = "disabled"
|
||||
UnresolvedNodeName = "unresolved"
|
||||
)
|
||||
|
||||
var instance *serviceMap
|
||||
var once sync.Once
|
||||
|
||||
func GetInstance() ServiceMap {
|
||||
once.Do(func() {
|
||||
instance = newServiceMap()
|
||||
logger.Log.Debug("Service Map Initialized")
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
type serviceMap struct {
|
||||
config *shared.MizuAgentConfig
|
||||
graph *graph
|
||||
entriesProcessed int
|
||||
}
|
||||
|
||||
type ServiceMap interface {
|
||||
SetConfig(config *shared.MizuAgentConfig)
|
||||
IsEnabled() bool
|
||||
NewTCPEntry(source *tapApi.TCP, destination *tapApi.TCP, protocol *tapApi.Protocol)
|
||||
GetStatus() ServiceMapStatus
|
||||
GetNodes() []ServiceMapNode
|
||||
GetEdges() []ServiceMapEdge
|
||||
GetEntriesProcessedCount() int
|
||||
GetNodesCount() int
|
||||
GetEdgesCount() int
|
||||
Reset()
|
||||
}
|
||||
|
||||
func newServiceMap() *serviceMap {
|
||||
return &serviceMap{
|
||||
config: nil,
|
||||
entriesProcessed: 0,
|
||||
graph: newDirectedGraph(),
|
||||
}
|
||||
}
|
||||
|
||||
type key string
|
||||
|
||||
type entryData struct {
|
||||
key key
|
||||
entry *tapApi.TCP
|
||||
}
|
||||
|
||||
type nodeData struct {
|
||||
id int
|
||||
entry *tapApi.TCP
|
||||
count int
|
||||
}
|
||||
|
||||
type edgeProtocol struct {
|
||||
protocol *tapApi.Protocol
|
||||
count int
|
||||
}
|
||||
|
||||
type edgeData struct {
|
||||
data map[key]*edgeProtocol
|
||||
}
|
||||
|
||||
type graph struct {
|
||||
Nodes map[key]*nodeData
|
||||
Edges map[key]map[key]*edgeData
|
||||
}
|
||||
|
||||
func newDirectedGraph() *graph {
|
||||
return &graph{
|
||||
Nodes: make(map[key]*nodeData),
|
||||
Edges: make(map[key]map[key]*edgeData),
|
||||
}
|
||||
}
|
||||
|
||||
func newNodeData(id int, e *tapApi.TCP) *nodeData {
|
||||
return &nodeData{
|
||||
id: id,
|
||||
entry: e,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func newEdgeData(p *tapApi.Protocol) *edgeData {
|
||||
return &edgeData{
|
||||
data: map[key]*edgeProtocol{
|
||||
key(p.Name): {
|
||||
protocol: p,
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceMap) nodeExists(k key) (*nodeData, bool) {
|
||||
n, ok := s.graph.Nodes[k]
|
||||
return n, ok
|
||||
}
|
||||
|
||||
func (s *serviceMap) addNode(k key, e *tapApi.TCP) (*nodeData, bool) {
|
||||
nd, exists := s.nodeExists(k)
|
||||
if !exists {
|
||||
s.graph.Nodes[k] = newNodeData(len(s.graph.Nodes)+1, e)
|
||||
return s.graph.Nodes[k], true
|
||||
}
|
||||
return nd, false
|
||||
}
|
||||
|
||||
func (s *serviceMap) addEdge(u, v *entryData, p *tapApi.Protocol) {
|
||||
if n, ok := s.addNode(u.key, u.entry); !ok {
|
||||
n.count++
|
||||
}
|
||||
if n, ok := s.addNode(v.key, v.entry); !ok {
|
||||
n.count++
|
||||
}
|
||||
|
||||
if _, ok := s.graph.Edges[u.key]; !ok {
|
||||
s.graph.Edges[u.key] = make(map[key]*edgeData)
|
||||
}
|
||||
|
||||
// new edge u -> v pair
|
||||
// protocol is the same for u and v
|
||||
if e, ok := s.graph.Edges[u.key][v.key]; ok {
|
||||
// edge data already exists for u -> v pair
|
||||
// we have a new protocol for this u -> v pair
|
||||
|
||||
k := key(p.Name)
|
||||
if pd, pOk := e.data[k]; pOk {
|
||||
// protocol key already exists, just increment the count
|
||||
pd.count++
|
||||
} else {
|
||||
// new protocol key
|
||||
e.data[k] = &edgeProtocol{
|
||||
protocol: p,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// new edge data for u -> v pair
|
||||
s.graph.Edges[u.key][v.key] = newEdgeData(p)
|
||||
}
|
||||
|
||||
s.entriesProcessed++
|
||||
}
|
||||
|
||||
func (s *serviceMap) SetConfig(config *shared.MizuAgentConfig) {
|
||||
s.config = config
|
||||
}
|
||||
|
||||
func (s *serviceMap) IsEnabled() bool {
|
||||
if s.config != nil && s.config.ServiceMap {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *serviceMap) NewTCPEntry(src *tapApi.TCP, dst *tapApi.TCP, p *tapApi.Protocol) {
|
||||
if !s.IsEnabled() {
|
||||
return
|
||||
}
|
||||
|
||||
srcEntry := &entryData{
|
||||
key: key(src.IP),
|
||||
entry: src,
|
||||
}
|
||||
if len(srcEntry.entry.Name) == 0 {
|
||||
srcEntry.entry.Name = UnresolvedNodeName
|
||||
}
|
||||
|
||||
dstEntry := &entryData{
|
||||
key: key(dst.IP),
|
||||
entry: dst,
|
||||
}
|
||||
if len(dstEntry.entry.Name) == 0 {
|
||||
dstEntry.entry.Name = UnresolvedNodeName
|
||||
}
|
||||
|
||||
s.addEdge(srcEntry, dstEntry, p)
|
||||
}
|
||||
|
||||
func (s *serviceMap) GetStatus() ServiceMapStatus {
|
||||
status := ServiceMapDisabled
|
||||
if s.IsEnabled() {
|
||||
status = ServiceMapEnabled
|
||||
}
|
||||
|
||||
return ServiceMapStatus{
|
||||
Status: status,
|
||||
EntriesProcessedCount: s.entriesProcessed,
|
||||
NodeCount: s.GetNodesCount(),
|
||||
EdgeCount: s.GetEdgesCount(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *serviceMap) GetNodes() []ServiceMapNode {
|
||||
var nodes []ServiceMapNode
|
||||
for i, n := range s.graph.Nodes {
|
||||
nodes = append(nodes, ServiceMapNode{
|
||||
Id: n.id,
|
||||
Name: string(i),
|
||||
Entry: n.entry,
|
||||
Count: n.count,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (s *serviceMap) GetEdges() []ServiceMapEdge {
|
||||
var edges []ServiceMapEdge
|
||||
for u, m := range s.graph.Edges {
|
||||
for v := range m {
|
||||
for _, p := range s.graph.Edges[u][v].data {
|
||||
edges = append(edges, ServiceMapEdge{
|
||||
Source: ServiceMapNode{
|
||||
Id: s.graph.Nodes[u].id,
|
||||
Name: string(u),
|
||||
Entry: s.graph.Nodes[u].entry,
|
||||
Count: s.graph.Nodes[u].count,
|
||||
},
|
||||
Destination: ServiceMapNode{
|
||||
Id: s.graph.Nodes[v].id,
|
||||
Name: string(v),
|
||||
Entry: s.graph.Nodes[v].entry,
|
||||
Count: s.graph.Nodes[v].count,
|
||||
},
|
||||
Count: p.count,
|
||||
Protocol: p.protocol,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return edges
|
||||
}
|
||||
|
||||
func (s *serviceMap) GetEntriesProcessedCount() int {
|
||||
return s.entriesProcessed
|
||||
}
|
||||
|
||||
func (s *serviceMap) GetNodesCount() int {
|
||||
return len(s.graph.Nodes)
|
||||
}
|
||||
|
||||
func (s *serviceMap) GetEdgesCount() int {
|
||||
var count int
|
||||
for u, m := range s.graph.Edges {
|
||||
for v := range m {
|
||||
for range s.graph.Edges[u][v].data {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func (s *serviceMap) Reset() {
|
||||
s.entriesProcessed = 0
|
||||
s.graph = newDirectedGraph()
|
||||
}
|
405
agent/pkg/servicemap/servicemap_test.go
Normal file
405
agent/pkg/servicemap/servicemap_test.go
Normal file
@ -0,0 +1,405 @@
|
||||
package servicemap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/up9inc/mizu/shared"
|
||||
tapApi "github.com/up9inc/mizu/tap/api"
|
||||
)
|
||||
|
||||
const (
|
||||
a = "aService"
|
||||
b = "bService"
|
||||
c = "cService"
|
||||
d = "dService"
|
||||
Ip = "127.0.0.1"
|
||||
Port = "80"
|
||||
)
|
||||
|
||||
var (
|
||||
TCPEntryA = &tapApi.TCP{
|
||||
Name: a,
|
||||
Port: Port,
|
||||
IP: fmt.Sprintf("%s.%s", Ip, a),
|
||||
}
|
||||
TCPEntryB = &tapApi.TCP{
|
||||
Name: b,
|
||||
Port: Port,
|
||||
IP: fmt.Sprintf("%s.%s", Ip, b),
|
||||
}
|
||||
TCPEntryC = &tapApi.TCP{
|
||||
Name: c,
|
||||
Port: Port,
|
||||
IP: fmt.Sprintf("%s.%s", Ip, c),
|
||||
}
|
||||
TCPEntryD = &tapApi.TCP{
|
||||
Name: d,
|
||||
Port: Port,
|
||||
IP: fmt.Sprintf("%s.%s", Ip, d),
|
||||
}
|
||||
TCPEntryUnresolved = &tapApi.TCP{
|
||||
Name: "",
|
||||
Port: Port,
|
||||
IP: Ip,
|
||||
}
|
||||
TCPEntryUnresolved2 = &tapApi.TCP{
|
||||
Name: "",
|
||||
Port: Port,
|
||||
IP: fmt.Sprintf("%s.%s", Ip, UnresolvedNodeName),
|
||||
}
|
||||
ProtocolHttp = &tapApi.Protocol{
|
||||
Name: "http",
|
||||
LongName: "Hypertext Transfer Protocol -- HTTP/1.1",
|
||||
Abbreviation: "HTTP",
|
||||
Macro: "http",
|
||||
Version: "1.1",
|
||||
BackgroundColor: "#205cf5",
|
||||
ForegroundColor: "#ffffff",
|
||||
FontSize: 12,
|
||||
ReferenceLink: "https://datatracker.ietf.org/doc/html/rfc2616",
|
||||
Ports: []string{"80", "443", "8080"},
|
||||
Priority: 0,
|
||||
}
|
||||
ProtocolRedis = &tapApi.Protocol{
|
||||
Name: "redis",
|
||||
LongName: "Redis Serialization Protocol",
|
||||
Abbreviation: "REDIS",
|
||||
Macro: "redis",
|
||||
Version: "3.x",
|
||||
BackgroundColor: "#a41e11",
|
||||
ForegroundColor: "#ffffff",
|
||||
FontSize: 11,
|
||||
ReferenceLink: "https://redis.io/topics/protocol",
|
||||
Ports: []string{"6379"},
|
||||
Priority: 3,
|
||||
}
|
||||
)
|
||||
|
||||
type ServiceMapDisabledSuite struct {
|
||||
suite.Suite
|
||||
|
||||
instance ServiceMap
|
||||
}
|
||||
|
||||
type ServiceMapEnabledSuite struct {
|
||||
suite.Suite
|
||||
|
||||
instance ServiceMap
|
||||
}
|
||||
|
||||
func (s *ServiceMapDisabledSuite) SetupTest() {
|
||||
s.instance = GetInstance()
|
||||
}
|
||||
|
||||
func (s *ServiceMapEnabledSuite) SetupTest() {
|
||||
s.instance = GetInstance()
|
||||
s.instance.SetConfig(&shared.MizuAgentConfig{
|
||||
ServiceMap: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ServiceMapDisabledSuite) TestServiceMapInstance() {
|
||||
assert := s.Assert()
|
||||
|
||||
assert.NotNil(s.instance)
|
||||
}
|
||||
|
||||
func (s *ServiceMapDisabledSuite) TestServiceMapSingletonInstance() {
|
||||
assert := s.Assert()
|
||||
|
||||
instance2 := GetInstance()
|
||||
|
||||
assert.NotNil(s.instance)
|
||||
assert.NotNil(instance2)
|
||||
assert.Equal(s.instance, instance2)
|
||||
}
|
||||
|
||||
func (s *ServiceMapDisabledSuite) TestServiceMapIsEnabledShouldReturnFalseByDefault() {
|
||||
assert := s.Assert()
|
||||
|
||||
enabled := s.instance.IsEnabled()
|
||||
|
||||
assert.False(enabled)
|
||||
}
|
||||
|
||||
func (s *ServiceMapDisabledSuite) TestGetStatusShouldReturnDisabledByDefault() {
|
||||
assert := s.Assert()
|
||||
|
||||
status := s.instance.GetStatus()
|
||||
|
||||
assert.Equal("disabled", status.Status)
|
||||
assert.Equal(0, status.EntriesProcessedCount)
|
||||
assert.Equal(0, status.NodeCount)
|
||||
assert.Equal(0, status.EdgeCount)
|
||||
}
|
||||
|
||||
func (s *ServiceMapDisabledSuite) TestNewTCPEntryShouldDoNothingWhenDisabled() {
|
||||
assert := s.Assert()
|
||||
|
||||
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||
s.instance.NewTCPEntry(TCPEntryC, TCPEntryD, ProtocolHttp)
|
||||
status := s.instance.GetStatus()
|
||||
|
||||
assert.Equal("disabled", status.Status)
|
||||
assert.Equal(0, status.EntriesProcessedCount)
|
||||
assert.Equal(0, status.NodeCount)
|
||||
assert.Equal(0, status.EdgeCount)
|
||||
}
|
||||
|
||||
// Enabled
|
||||
|
||||
func (s *ServiceMapEnabledSuite) TestServiceMapIsEnabled() {
|
||||
assert := s.Assert()
|
||||
|
||||
enabled := s.instance.IsEnabled()
|
||||
|
||||
assert.True(enabled)
|
||||
}
|
||||
|
||||
func (s *ServiceMapEnabledSuite) TestServiceMap() {
|
||||
assert := s.Assert()
|
||||
|
||||
// A -> B - HTTP
|
||||
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||
|
||||
nodes := s.instance.GetNodes()
|
||||
edges := s.instance.GetEdges()
|
||||
|
||||
// Counts for the first entry
|
||||
assert.Equal(1, s.instance.GetEntriesProcessedCount())
|
||||
assert.Equal(2, s.instance.GetNodesCount())
|
||||
assert.Equal(2, len(nodes))
|
||||
assert.Equal(1, s.instance.GetEdgesCount())
|
||||
assert.Equal(1, len(edges))
|
||||
//http protocol
|
||||
assert.Equal(1, edges[0].Count)
|
||||
assert.Equal(ProtocolHttp.Name, edges[0].Protocol.Name)
|
||||
|
||||
// same A -> B - HTTP, http protocol count should be 2, edges count should be 1
|
||||
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolHttp)
|
||||
|
||||
nodes = s.instance.GetNodes()
|
||||
edges = s.instance.GetEdges()
|
||||
|
||||
// Counts for a second entry
|
||||
assert.Equal(2, s.instance.GetEntriesProcessedCount())
|
||||
assert.Equal(2, s.instance.GetNodesCount())
|
||||
assert.Equal(2, len(nodes))
|
||||
// edges count should still be 1, but http protocol count should be 2
|
||||
assert.Equal(1, s.instance.GetEdgesCount())
|
||||
assert.Equal(1, len(edges))
|
||||
// http protocol
|
||||
assert.Equal(2, edges[0].Count) //http
|
||||
assert.Equal(ProtocolHttp.Name, edges[0].Protocol.Name)
|
||||
|
||||
// same A -> B - REDIS, http protocol count should be 2 and redis protocol count should 1, edges count should be 2
|
||||
s.instance.NewTCPEntry(TCPEntryA, TCPEntryB, ProtocolRedis)
|
||||
|
||||
nodes = s.instance.GetNodes()
|
||||
edges = s.instance.GetEdges()
|
||||
|
||||
// Counts after second entry
|
||||
assert.Equal(3, s.instance.GetEntriesProcessedCount())
|
||||
assert.Equal(2, s.instance.GetNodesCount())
|
||||
assert.Equal(2, len(nodes))
|
||||
// edges count should be 2, http protocol count should be 2 and redis protocol should be 1
|
||||
assert.Equal(2, s.instance.GetEdgesCount())
|
||||
assert.Equal(2, len(edges))
|
||||
// http and redis protocols
|
||||
httpIndex := -1
|
||||
redisIndex := -1
|
||||
for i, e := range edges {
|
||||
if e.Protocol.Name == ProtocolHttp.Name {
|
||||
httpIndex = i
|
||||
continue
|
||||
}
|
||||
if e.Protocol.Name == ProtocolRedis.Name {
|
||||
redisIndex = i
|
||||
}
|
||||
}
|
||||
assert.NotEqual(-1, httpIndex)
|
||||
assert.NotEqual(-1, redisIndex)
|
||||
// http protocol
|
||||
assert.Equal(2, edges[httpIndex].Count)
|
||||
assert.Equal(ProtocolHttp.Name, edges[httpIndex].Protocol.Name)
|
||||
// redis protocol
|
||||
assert.Equal(1, edges[redisIndex].Count)
|
||||
assert.Equal(ProtocolRedis.Name, edges[redisIndex].Protocol.Name)
|
||||
|
||||
// other entries
|
||||
s.instance.NewTCPEntry(TCPEntryUnresolved, TCPEntryA, ProtocolHttp)
|
||||
s.instance.NewTCPEntry(TCPEntryB, TCPEntryUnresolved2, ProtocolHttp)
|
||||
s.instance.NewTCPEntry(TCPEntryC, TCPEntryD, ProtocolHttp)
|
||||
s.instance.NewTCPEntry(TCPEntryA, TCPEntryC, ProtocolHttp)
|
||||
|
||||
status := s.instance.GetStatus()
|
||||
nodes = s.instance.GetNodes()
|
||||
edges = s.instance.GetEdges()
|
||||
expectedEntriesProcessedCount := 7
|
||||
expectedNodeCount := 6
|
||||
expectedEdgeCount := 6
|
||||
|
||||
// Counts after all entries
|
||||
assert.Equal(expectedEntriesProcessedCount, s.instance.GetEntriesProcessedCount())
|
||||
assert.Equal(expectedNodeCount, s.instance.GetNodesCount())
|
||||
assert.Equal(expectedNodeCount, len(nodes))
|
||||
assert.Equal(expectedEdgeCount, s.instance.GetEdgesCount())
|
||||
assert.Equal(expectedEdgeCount, len(edges))
|
||||
|
||||
// Status
|
||||
assert.Equal("enabled", status.Status)
|
||||
assert.Equal(expectedEntriesProcessedCount, status.EntriesProcessedCount)
|
||||
assert.Equal(expectedNodeCount, status.NodeCount)
|
||||
assert.Equal(expectedEdgeCount, status.EdgeCount)
|
||||
|
||||
// Nodes
|
||||
aNode := -1
|
||||
bNode := -1
|
||||
cNode := -1
|
||||
dNode := -1
|
||||
unresolvedNode := -1
|
||||
unresolvedNode2 := -1
|
||||
var validateNode = func(node ServiceMapNode, entryName string, count int) int {
|
||||
// id
|
||||
assert.GreaterOrEqual(node.Id, 1)
|
||||
assert.LessOrEqual(node.Id, expectedNodeCount)
|
||||
|
||||
// entry
|
||||
// node.Name is the key of the node, key = entry.IP
|
||||
// entry.Name is the name of the service and could be unresolved
|
||||
assert.Equal(node.Name, node.Entry.IP)
|
||||
assert.Equal(Port, node.Entry.Port)
|
||||
assert.Equal(entryName, node.Entry.Name)
|
||||
|
||||
// count
|
||||
assert.Equal(count, node.Count)
|
||||
|
||||
return node.Id
|
||||
}
|
||||
|
||||
for _, v := range nodes {
|
||||
if strings.HasSuffix(v.Name, a) {
|
||||
aNode = validateNode(v, a, 5)
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(v.Name, b) {
|
||||
bNode = validateNode(v, b, 4)
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(v.Name, c) {
|
||||
cNode = validateNode(v, c, 2)
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(v.Name, d) {
|
||||
dNode = validateNode(v, d, 1)
|
||||
continue
|
||||
}
|
||||
if v.Name == Ip {
|
||||
unresolvedNode = validateNode(v, UnresolvedNodeName, 1)
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(v.Name, UnresolvedNodeName) {
|
||||
unresolvedNode2 = validateNode(v, UnresolvedNodeName, 1)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we found all the nodes
|
||||
nodeIds := [...]int{aNode, bNode, cNode, dNode, unresolvedNode, unresolvedNode2}
|
||||
for _, v := range nodeIds {
|
||||
assert.NotEqual(-1, v)
|
||||
}
|
||||
|
||||
// Edges
|
||||
abEdge := -1
|
||||
uaEdge := -1
|
||||
buEdge := -1
|
||||
cdEdge := -1
|
||||
acEdge := -1
|
||||
var validateEdge = func(edge ServiceMapEdge, sourceEntryName string, destEntryName string, protocolName string, protocolCount int) {
|
||||
// source
|
||||
assert.Contains(nodeIds, edge.Source.Id)
|
||||
assert.LessOrEqual(edge.Source.Id, expectedNodeCount)
|
||||
assert.Equal(edge.Source.Name, edge.Source.Entry.IP)
|
||||
assert.Equal(sourceEntryName, edge.Source.Entry.Name)
|
||||
|
||||
// destination
|
||||
assert.Contains(nodeIds, edge.Destination.Id)
|
||||
assert.LessOrEqual(edge.Destination.Id, expectedNodeCount)
|
||||
assert.Equal(edge.Destination.Name, edge.Destination.Entry.IP)
|
||||
assert.Equal(destEntryName, edge.Destination.Entry.Name)
|
||||
|
||||
// protocol
|
||||
assert.Equal(protocolName, edge.Protocol.Name)
|
||||
assert.Equal(protocolCount, edge.Count)
|
||||
}
|
||||
|
||||
for i, v := range edges {
|
||||
if v.Source.Entry.Name == a && v.Destination.Entry.Name == b && v.Protocol.Name == "http" {
|
||||
validateEdge(v, a, b, ProtocolHttp.Name, 2)
|
||||
abEdge = i
|
||||
continue
|
||||
}
|
||||
if v.Source.Entry.Name == a && v.Destination.Entry.Name == b && v.Protocol.Name == "redis" {
|
||||
validateEdge(v, a, b, ProtocolRedis.Name, 1)
|
||||
abEdge = i
|
||||
continue
|
||||
}
|
||||
if v.Source.Entry.Name == UnresolvedNodeName && v.Destination.Entry.Name == a {
|
||||
validateEdge(v, UnresolvedNodeName, a, ProtocolHttp.Name, 1)
|
||||
uaEdge = i
|
||||
continue
|
||||
}
|
||||
if v.Source.Entry.Name == b && v.Destination.Entry.Name == UnresolvedNodeName {
|
||||
validateEdge(v, b, UnresolvedNodeName, ProtocolHttp.Name, 1)
|
||||
buEdge = i
|
||||
continue
|
||||
}
|
||||
if v.Source.Entry.Name == c && v.Destination.Entry.Name == d {
|
||||
validateEdge(v, c, d, ProtocolHttp.Name, 1)
|
||||
cdEdge = i
|
||||
continue
|
||||
}
|
||||
if v.Source.Entry.Name == a && v.Destination.Entry.Name == c {
|
||||
validateEdge(v, a, c, ProtocolHttp.Name, 1)
|
||||
acEdge = i
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we found all the edges
|
||||
for _, v := range [...]int{abEdge, uaEdge, buEdge, cdEdge, acEdge} {
|
||||
assert.NotEqual(-1, v)
|
||||
}
|
||||
|
||||
// Reset
|
||||
s.instance.Reset()
|
||||
status = s.instance.GetStatus()
|
||||
nodes = s.instance.GetNodes()
|
||||
edges = s.instance.GetEdges()
|
||||
|
||||
// Counts after reset
|
||||
assert.Equal(0, s.instance.GetEntriesProcessedCount())
|
||||
assert.Equal(0, s.instance.GetNodesCount())
|
||||
assert.Equal(0, s.instance.GetEdgesCount())
|
||||
|
||||
// Status after reset
|
||||
assert.Equal("enabled", status.Status)
|
||||
assert.Equal(0, status.EntriesProcessedCount)
|
||||
assert.Equal(0, status.NodeCount)
|
||||
assert.Equal(0, status.EdgeCount)
|
||||
|
||||
// Nodes after reset
|
||||
assert.Equal([]ServiceMapNode(nil), nodes)
|
||||
|
||||
// Edges after reset
|
||||
assert.Equal([]ServiceMapEdge(nil), edges)
|
||||
}
|
||||
|
||||
func TestServiceMapSuite(t *testing.T) {
|
||||
suite.Run(t, new(ServiceMapDisabledSuite))
|
||||
suite.Run(t, new(ServiceMapEnabledSuite))
|
||||
}
|
29
ui/package-lock.json
generated
29
ui/package-lock.json
generated
@ -14479,6 +14479,25 @@
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz",
|
||||
"integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew=="
|
||||
},
|
||||
"react-graph-vis": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-graph-vis/-/react-graph-vis-1.0.7.tgz",
|
||||
"integrity": "sha512-FI35zlBMKU22JEvG1ukd1DDwW185y4YrDvHm6Bom9EGdA+UNMrZrIV/lyPIRWPcRkzbKaA1w1NvOYcRApD4KdQ==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.15",
|
||||
"prop-types": "^15.5.10",
|
||||
"uuid": "^2.0.1",
|
||||
"vis-data": "^7.1.2",
|
||||
"vis-network": "^9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
|
||||
"integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@ -17902,6 +17921,16 @@
|
||||
"unist-util-stringify-position": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"vis-data": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vis-data/-/vis-data-7.1.2.tgz",
|
||||
"integrity": "sha512-RPSegFxEcnp3HUEJSzhS2vBdbJ2PSsrYYuhRlpHp2frO/MfRtTYbIkkLZmPkA/Sg3pPfBlR235gcoKbtdm4mbw=="
|
||||
},
|
||||
"vis-network": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vis-network/-/vis-network-9.1.0.tgz",
|
||||
"integrity": "sha512-rx96L144RJWcqOa6afjiFyxZKUerRRbT/YaNMpsusHdwzxrVTO2LlduR45PeJDEztrAf3AU5l2zmiG+1ydUZCw=="
|
||||
},
|
||||
"vm-browserify": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
|
||||
|
@ -30,6 +30,7 @@
|
||||
"react": "^17.0.2",
|
||||
"react-copy-to-clipboard": "^5.0.3",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-graph-vis": "^1.0.7",
|
||||
"react-lowlight": "^3.0.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-scrollable-feed-virtualized": "^1.4.9",
|
||||
|
@ -30,6 +30,7 @@
|
||||
// Injected from server
|
||||
window.isEnt = __IS_STANDALONE__
|
||||
window.isOasEnabled = __IS_OAS_ENABLED__
|
||||
window.isServiceMapEnabled = __IS_SERVICE_MAP_ENABLED__
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import './App.sass';
|
||||
import {TLSWarning} from "./components/TLSWarning/TLSWarning";
|
||||
import {Header} from "./components/Header/Header";
|
||||
import {TrafficPage} from "./components/TrafficPage";
|
||||
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||
|
||||
const App = () => {
|
||||
|
||||
@ -10,6 +11,7 @@ const App = () => {
|
||||
const [showTLSWarning, setShowTLSWarning] = useState(false);
|
||||
const [userDismissedTLSWarning, setUserDismissedTLSWarning] = useState(false);
|
||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
||||
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
|
||||
|
||||
const onTLSDetected = (destAddress: string) => {
|
||||
addressesWithTLS.add(destAddress);
|
||||
@ -22,14 +24,19 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<div className="mizuApp">
|
||||
<Header analyzeStatus={analyzeStatus}/>
|
||||
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected}/>
|
||||
<Header analyzeStatus={analyzeStatus} />
|
||||
<TrafficPage setAnalyzeStatus={setAnalyzeStatus} onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />
|
||||
<TLSWarning showTLSWarning={showTLSWarning}
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />
|
||||
{window["isServiceMapEnabled"] && <ServiceMapModal
|
||||
isOpen={openServiceMapModal}
|
||||
onOpen={() => setOpenServiceMapModal(true)}
|
||||
onClose={() => setOpenServiceMapModal(false)}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import LoadingOverlay from "./components/LoadingOverlay";
|
||||
import AuthPageBase from './components/AuthPageBase';
|
||||
import entPageAtom, {Page} from "./recoil/entPage";
|
||||
import {useRecoilState} from "recoil";
|
||||
import { ServiceMapModal } from './components/ServiceMapModal/ServiceMapModal';
|
||||
|
||||
const api = Api.getInstance();
|
||||
|
||||
@ -22,6 +23,7 @@ const EntApp = () => {
|
||||
const [addressesWithTLS, setAddressesWithTLS] = useState(new Set<string>());
|
||||
const [entPage, setEntPage] = useRecoilState(entPageAtom);
|
||||
const [isFirstLogin, setIsFirstLogin] = useState(false);
|
||||
const [openServiceMapModal, setOpenServiceMapModal] = useState(false);
|
||||
|
||||
const determinePage = useCallback(async () => { // TODO: move to state management
|
||||
try {
|
||||
@ -59,7 +61,7 @@ const EntApp = () => {
|
||||
|
||||
switch (entPage) { // TODO: move to state management / proper routing
|
||||
case Page.Traffic:
|
||||
pageComponent = <TrafficPage onTLSDetected={onTLSDetected}/>;
|
||||
pageComponent = <TrafficPage onTLSDetected={onTLSDetected} setOpenServiceMapModal={setOpenServiceMapModal} />;
|
||||
break;
|
||||
case Page.Setup:
|
||||
pageComponent = <AuthPageBase><InstallPage onFirstLogin={() => setIsFirstLogin(true)}/></AuthPageBase>;
|
||||
@ -77,14 +79,19 @@ const EntApp = () => {
|
||||
|
||||
return (
|
||||
<div className="mizuApp">
|
||||
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin}/>}
|
||||
{entPage === Page.Traffic && <EntHeader isFirstLogin={isFirstLogin} setIsFirstLogin={setIsFirstLogin} />}
|
||||
{pageComponent}
|
||||
{entPage === Page.Traffic && <TLSWarning showTLSWarning={showTLSWarning}
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning}/>}
|
||||
setShowTLSWarning={setShowTLSWarning}
|
||||
addressesWithTLS={addressesWithTLS}
|
||||
setAddressesWithTLS={setAddressesWithTLS}
|
||||
userDismissedTLSWarning={userDismissedTLSWarning}
|
||||
setUserDismissedTLSWarning={setUserDismissedTLSWarning} />}
|
||||
{entPage === Page.Traffic && window["isServiceMapEnabled"] && <ServiceMapModal
|
||||
isOpen={openServiceMapModal}
|
||||
onOpen={() => setOpenServiceMapModal(true)}
|
||||
onClose={() => setOpenServiceMapModal(false)}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
216
ui/src/components/ServiceMapModal/ServiceMapModal.tsx
Normal file
216
ui/src/components/ServiceMapModal/ServiceMapModal.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Box, Fade, Modal, Backdrop, Button } from "@material-ui/core";
|
||||
import { toast } from "react-toastify";
|
||||
import Api from "../../helpers/api";
|
||||
import spinnerStyle from '../style/Spinner.module.sass';
|
||||
import spinnerImg from '../assets/spinner.svg';
|
||||
import Graph from "react-graph-vis";
|
||||
import debounce from 'lodash/debounce';
|
||||
import ServiceMapOptions from './ServiceMapOptions'
|
||||
import { useCommonStyles } from "../../helpers/commonStyle";
|
||||
|
||||
interface GraphData {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: number;
|
||||
value: number;
|
||||
label: string;
|
||||
title?: string;
|
||||
color?: object;
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
from: number;
|
||||
to: number;
|
||||
value: number;
|
||||
label: string;
|
||||
title?: string;
|
||||
color?: object;
|
||||
}
|
||||
|
||||
interface ServiceMapNode {
|
||||
id: number;
|
||||
name: string;
|
||||
entry: Entry;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ServiceMapEdge {
|
||||
source: ServiceMapNode;
|
||||
destination: ServiceMapNode;
|
||||
count: number;
|
||||
protocol: Protocol;
|
||||
}
|
||||
|
||||
interface ServiceMapGraph {
|
||||
nodes: ServiceMapNode[];
|
||||
edges: ServiceMapEdge[];
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
ip: string;
|
||||
port: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Protocol {
|
||||
name: string;
|
||||
abbr: string;
|
||||
macro: string;
|
||||
version: string;
|
||||
backgroundColor: string;
|
||||
foregroundColor: string;
|
||||
fontSize: number;
|
||||
referenceLink: string;
|
||||
ports: string[];
|
||||
priority: number;
|
||||
}
|
||||
|
||||
interface ServiceMapModalProps {
|
||||
isOpen: boolean;
|
||||
onOpen: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const modalStyle = {
|
||||
position: 'absolute',
|
||||
top: '10%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, 0%)',
|
||||
width: '80vw',
|
||||
height: '80vh',
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: '5px',
|
||||
boxShadow: 24,
|
||||
p: 4,
|
||||
color: '#000',
|
||||
};
|
||||
|
||||
const api = Api.getInstance();
|
||||
|
||||
export const ServiceMapModal: React.FC<ServiceMapModalProps> = ({ isOpen, onOpen, onClose }) => {
|
||||
const commonClasses = useCommonStyles();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] });
|
||||
|
||||
const getServiceMapData = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
|
||||
const serviceMapData: ServiceMapGraph = await api.serviceMapData()
|
||||
const newGraphData: GraphData = { nodes: [], edges: [] }
|
||||
|
||||
if (serviceMapData.nodes) {
|
||||
newGraphData.nodes = serviceMapData.nodes.map(node => {
|
||||
return {
|
||||
id: node.id,
|
||||
value: node.count,
|
||||
label: (node.entry.name === "unresolved") ? node.name : `${node.entry.name} (${node.name})`,
|
||||
title: "Count: " + node.name,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (serviceMapData.edges) {
|
||||
newGraphData.edges = serviceMapData.edges.map(edge => {
|
||||
return {
|
||||
from: edge.source.id,
|
||||
to: edge.destination.id,
|
||||
value: edge.count,
|
||||
label: edge.count.toString(),
|
||||
color: {
|
||||
color: edge.protocol.backgroundColor,
|
||||
highlight: edge.protocol.backgroundColor
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setGraphData(newGraphData)
|
||||
|
||||
} catch (ex) {
|
||||
toast.error("An error occurred while loading Mizu Service Map, see console for mode details");
|
||||
console.error(ex);
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
getServiceMapData()
|
||||
}, [getServiceMapData])
|
||||
|
||||
const resetServiceMap = debounce(async () => {
|
||||
try {
|
||||
const serviceMapResetResponse = await api.serviceMapReset();
|
||||
if (serviceMapResetResponse["status"] === "enabled") {
|
||||
refreshServiceMap()
|
||||
}
|
||||
|
||||
} catch (ex) {
|
||||
toast.error("An error occurred while resetting Mizu Service Map, see console for mode details");
|
||||
console.error(ex);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const refreshServiceMap = debounce(() => {
|
||||
getServiceMapData();
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
open={isOpen}
|
||||
onClose={onClose}
|
||||
closeAfterTransition
|
||||
BackdropComponent={Backdrop}
|
||||
BackdropProps={{
|
||||
timeout: 500,
|
||||
}}
|
||||
style={{ overflow: 'auto' }}
|
||||
>
|
||||
<Fade in={isOpen}>
|
||||
<Box sx={modalStyle}>
|
||||
{isLoading && <div className={spinnerStyle.spinnerContainer}>
|
||||
<img alt="spinner" src={spinnerImg} style={{ height: 50 }} />
|
||||
</div>}
|
||||
{!isLoading && <div style={{ height: "100%", width: "100%" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={commonClasses.button}
|
||||
style={{ marginRight: 25 }}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={commonClasses.button}
|
||||
style={{ marginRight: 25 }}
|
||||
onClick={resetServiceMap}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={commonClasses.button}
|
||||
onClick={refreshServiceMap}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Graph
|
||||
graph={graphData}
|
||||
options={ServiceMapOptions}
|
||||
/>
|
||||
</div>}
|
||||
</Box>
|
||||
</Fade>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
}
|
83
ui/src/components/ServiceMapModal/ServiceMapOptions.ts
Normal file
83
ui/src/components/ServiceMapModal/ServiceMapOptions.ts
Normal file
@ -0,0 +1,83 @@
|
||||
const ServiceMapOptions = {
|
||||
physics: {
|
||||
enabled: true,
|
||||
solver: 'barnesHut',
|
||||
barnesHut: {
|
||||
theta: 0.5,
|
||||
gravitationalConstant: -2000,
|
||||
centralGravity: 0.3,
|
||||
springLength: 180,
|
||||
springConstant: 0.04,
|
||||
damping: 0.09,
|
||||
avoidOverlap: 1
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
hierarchical: false,
|
||||
randomSeed: 1 // always on node 1
|
||||
},
|
||||
nodes: {
|
||||
shape: 'dot',
|
||||
chosen: true,
|
||||
color: {
|
||||
background: '#27AE60',
|
||||
border: '#000000',
|
||||
highlight: {
|
||||
background: '#27AE60',
|
||||
border: '#000000',
|
||||
},
|
||||
},
|
||||
font: {
|
||||
color: '#343434',
|
||||
size: 14, // px
|
||||
face: 'arial',
|
||||
background: 'none',
|
||||
strokeWidth: 0, // px
|
||||
strokeColor: '#ffffff',
|
||||
align: 'center',
|
||||
multi: false,
|
||||
},
|
||||
borderWidth: 1.5,
|
||||
borderWidthSelected: 2.5,
|
||||
labelHighlightBold: true,
|
||||
opacity: 1,
|
||||
shadow: true,
|
||||
},
|
||||
edges: {
|
||||
chosen: true,
|
||||
dashes: false,
|
||||
arrowStrikethrough: false,
|
||||
arrows: {
|
||||
to: {
|
||||
enabled: true,
|
||||
},
|
||||
middle: {
|
||||
enabled: false,
|
||||
},
|
||||
from: {
|
||||
enabled: false,
|
||||
}
|
||||
},
|
||||
smooth: {
|
||||
enabled: true,
|
||||
type: 'dynamic',
|
||||
roundness: 1.0
|
||||
},
|
||||
font: {
|
||||
color: '#343434',
|
||||
size: 12, // px
|
||||
face: 'arial',
|
||||
background: 'none',
|
||||
strokeWidth: 2, // px
|
||||
strokeColor: '#ffffff',
|
||||
align: 'horizontal',
|
||||
multi: false,
|
||||
},
|
||||
labelHighlightBold: true,
|
||||
selectionWidth: 1,
|
||||
shadow: true,
|
||||
},
|
||||
autoResize: true,
|
||||
};
|
||||
|
||||
export default ServiceMapOptions
|
@ -19,6 +19,7 @@ import focusedEntryIdAtom from "../recoil/focusedEntryId";
|
||||
import websocketConnectionAtom, {WsConnectionStatus} from "../recoil/wsConnection";
|
||||
import queryAtom from "../recoil/query";
|
||||
import OasModal from "./OasModal/OasModal";
|
||||
import {useCommonStyles} from "../helpers/commonStyle"
|
||||
|
||||
const useLayoutStyles = makeStyles(() => ({
|
||||
details: {
|
||||
@ -43,11 +44,13 @@ const useLayoutStyles = makeStyles(() => ({
|
||||
interface TrafficPageProps {
|
||||
setAnalyzeStatus?: (status: any) => void;
|
||||
onTLSDetected: (destAddress: string) => void;
|
||||
setOpenServiceMapModal?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const api = Api.getInstance();
|
||||
|
||||
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSDetected}) => {
|
||||
export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSDetected, setOpenServiceMapModal}) => {
|
||||
const commonClasses = useCommonStyles();
|
||||
const classes = useLayoutStyles();
|
||||
const [tappingStatus, setTappingStatus] = useRecoilState(tappingStatusAtom);
|
||||
const [entries, setEntries] = useRecoilState(entriesAtom);
|
||||
@ -239,79 +242,84 @@ export const TrafficPage: React.FC<TrafficPageProps> = ({setAnalyzeStatus,onTLSD
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="TrafficPage">
|
||||
<div className="TrafficPageHeader">
|
||||
<div className="TrafficPageStreamStatus">
|
||||
<img className="playPauseIcon" style={{visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden"}} alt="pause"
|
||||
src={pauseIcon} onClick={toggleConnection}/>
|
||||
<img className="playPauseIcon" style={{position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible"}} alt="play"
|
||||
src={playIcon} onClick={toggleConnection}/>
|
||||
<div className="connectionText">
|
||||
{getConnectionTitle()}
|
||||
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
|
||||
<div className={"indicator " + getConnectionStatusClass(false)}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{window["isOasEnabled"] && <div>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
style={{
|
||||
margin: "2px 0px 0px 0px",
|
||||
backgroundColor: variables.blueColor,
|
||||
fontWeight: 600,
|
||||
borderRadius: "4px",
|
||||
color: "#fff",
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
Show OAS
|
||||
</Button>
|
||||
</div>}
|
||||
const openServiceMapModalDebounce = debounce(() => {
|
||||
setOpenServiceMapModal(true)
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<div className="TrafficPage">
|
||||
<div className="TrafficPageHeader">
|
||||
<div className="TrafficPageStreamStatus">
|
||||
<img className="playPauseIcon" style={{ visibility: wsConnection === WsConnectionStatus.Connected ? "visible" : "hidden" }} alt="pause"
|
||||
src={pauseIcon} onClick={toggleConnection} />
|
||||
<img className="playPauseIcon" style={{ position: "absolute", visibility: wsConnection === WsConnectionStatus.Connected ? "hidden" : "visible" }} alt="play"
|
||||
src={playIcon} onClick={toggleConnection} />
|
||||
<div className="connectionText">
|
||||
{getConnectionTitle()}
|
||||
<div className={"indicatorContainer " + getConnectionStatusClass(true)}>
|
||||
<div className={"indicator " + getConnectionStatusClass(false)} />
|
||||
</div>
|
||||
{window["isOasEnabled"] && <OasModal
|
||||
openModal={openOasModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>}
|
||||
{<div className="TrafficPage-Container">
|
||||
<div className="TrafficPage-ListContainer">
|
||||
<Filters
|
||||
backgroundColor={queryBackgroundColor}
|
||||
ws={ws.current}
|
||||
openWebSocket={openWebSocket}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<EntriesList
|
||||
listEntryREF={listEntry}
|
||||
onSnapBrokenEvent={onSnapBrokenEvent}
|
||||
isSnappedToBottom={isSnappedToBottom}
|
||||
setIsSnappedToBottom={setIsSnappedToBottom}
|
||||
queriedCurrent={queriedCurrent}
|
||||
setQueriedCurrent={setQueriedCurrent}
|
||||
queriedTotal={queriedTotal}
|
||||
setQueriedTotal={setQueriedTotal}
|
||||
startTime={startTime}
|
||||
noMoreDataTop={noMoreDataTop}
|
||||
setNoMoreDataTop={setNoMoreDataTop}
|
||||
leftOffTop={leftOffTop}
|
||||
setLeftOffTop={setLeftOffTop}
|
||||
ws={ws.current}
|
||||
openWebSocket={openWebSocket}
|
||||
leftOffBottom={leftOffBottom}
|
||||
truncatedTimestamp={truncatedTimestamp}
|
||||
setTruncatedTimestamp={setTruncatedTimestamp}
|
||||
scrollableRef={scrollableRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.details}>
|
||||
{focusedEntryId && <EntryDetailed/>}
|
||||
</div>
|
||||
</div>}
|
||||
{tappingStatus && !openOasModal && <StatusBar/>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{window["isOasEnabled"] && <Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
className={commonClasses.button}
|
||||
style={{ marginRight: 25 }}
|
||||
onClick={handleOpenModal}
|
||||
>
|
||||
Show OAS
|
||||
</Button>}
|
||||
{window["isServiceMapEnabled"] && <Button
|
||||
variant="contained"
|
||||
className={commonClasses.button}
|
||||
onClick={openServiceMapModalDebounce}
|
||||
>
|
||||
Service Map
|
||||
</Button>}
|
||||
</div>
|
||||
</div>
|
||||
{window["isOasEnabled"] && <OasModal
|
||||
openModal={openOasModal}
|
||||
handleCloseModal={handleCloseModal}
|
||||
/>}
|
||||
{<div className="TrafficPage-Container">
|
||||
<div className="TrafficPage-ListContainer">
|
||||
<Filters
|
||||
backgroundColor={queryBackgroundColor}
|
||||
ws={ws.current}
|
||||
openWebSocket={openWebSocket}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<EntriesList
|
||||
listEntryREF={listEntry}
|
||||
onSnapBrokenEvent={onSnapBrokenEvent}
|
||||
isSnappedToBottom={isSnappedToBottom}
|
||||
setIsSnappedToBottom={setIsSnappedToBottom}
|
||||
queriedCurrent={queriedCurrent}
|
||||
setQueriedCurrent={setQueriedCurrent}
|
||||
queriedTotal={queriedTotal}
|
||||
setQueriedTotal={setQueriedTotal}
|
||||
startTime={startTime}
|
||||
noMoreDataTop={noMoreDataTop}
|
||||
setNoMoreDataTop={setNoMoreDataTop}
|
||||
leftOffTop={leftOffTop}
|
||||
setLeftOffTop={setLeftOffTop}
|
||||
ws={ws.current}
|
||||
openWebSocket={openWebSocket}
|
||||
leftOffBottom={leftOffBottom}
|
||||
truncatedTimestamp={truncatedTimestamp}
|
||||
setTruncatedTimestamp={setTruncatedTimestamp}
|
||||
scrollableRef={scrollableRef}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.details}>
|
||||
{focusedEntryId && <EntryDetailed />}
|
||||
</div>
|
||||
</div>}
|
||||
{tappingStatus && !openOasModal && <StatusBar />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
7
ui/src/components/style/Spinner.module.sass
Normal file
7
ui/src/components/style/Spinner.module.sass
Normal file
@ -0,0 +1,7 @@
|
||||
@import "../../variables.module"
|
||||
|
||||
.spinnerContainer
|
||||
display: flex
|
||||
justify-content: center
|
||||
margin-bottom: 10px
|
||||
|
@ -28,6 +28,21 @@ export default class Api {
|
||||
this.source = null;
|
||||
}
|
||||
|
||||
serviceMapStatus = async () => {
|
||||
const response = await this.client.get("/servicemap/status");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
serviceMapData = async () => {
|
||||
const response = await this.client.get(`/servicemap/get`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
serviceMapReset = async () => {
|
||||
const response = await this.client.get(`/servicemap/reset`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
tapStatus = async () => {
|
||||
const response = await this.client.get("/status/tap");
|
||||
return response.data;
|
||||
|
Loading…
Reference in New Issue
Block a user