mirror of
https://github.com/akvorado/akvorado.git
synced 2025-12-11 22:14:02 +01:00
``` git ls-files \*.js \*.go \ | xargs sed -i '1i // SPDX-FileCopyrightText: 2022 Free Mobile\n// SPDX-License-Identifier: AGPL-3.0-only\n' git ls-files \*.vue \ | xargs sed -i '1i <!-- SPDX-FileCopyrightText: 2022 Free Mobile -->\n<!-- SPDX-License-Identifier: AGPL-3.0-only -->\n' ```
325 lines
9.8 KiB
Go
325 lines
9.8 KiB
Go
// SPDX-FileCopyrightText: 2022 Free Mobile
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
package console
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"akvorado/common/helpers"
|
|
"akvorado/console/authentication"
|
|
"akvorado/console/database"
|
|
"akvorado/console/filter"
|
|
)
|
|
|
|
// filterValidateHandlerInput describes the input for the /filter/validate endpoint.
|
|
type filterValidateHandlerInput struct {
|
|
Filter string `json:"filter"`
|
|
}
|
|
|
|
// filterValidateHandlerOutput describes the output for the /filter/validate endpoint.
|
|
type filterValidateHandlerOutput struct {
|
|
Message string `json:"message"`
|
|
Parsed string `json:"parsed,omitempty"`
|
|
Errors filter.Errors `json:"errors,omitempty"`
|
|
}
|
|
|
|
func (c *Component) filterValidateHandlerFunc(gc *gin.Context) {
|
|
var input filterValidateHandlerInput
|
|
if err := gc.ShouldBindJSON(&input); err != nil {
|
|
gc.JSON(http.StatusBadRequest, gin.H{"message": helpers.Capitalize(err.Error())})
|
|
return
|
|
}
|
|
|
|
if strings.TrimSpace(input.Filter) == "" {
|
|
gc.JSON(http.StatusOK, filterValidateHandlerOutput{
|
|
Message: "ok",
|
|
})
|
|
return
|
|
}
|
|
got, err := filter.Parse("", []byte(input.Filter))
|
|
if err == nil {
|
|
gc.JSON(http.StatusOK, filterValidateHandlerOutput{
|
|
Message: "ok",
|
|
Parsed: got.(string),
|
|
})
|
|
return
|
|
}
|
|
gc.JSON(http.StatusOK, filterValidateHandlerOutput{
|
|
Message: filter.HumanError(err),
|
|
Errors: filter.AllErrors(err),
|
|
})
|
|
}
|
|
|
|
// filterCompleteHandlerInput describes the input of the /filter/complete endpoint.
|
|
type filterCompleteHandlerInput struct {
|
|
What string `json:"what" binding:"required,oneof=column operator value"`
|
|
Column string `json:"column" binding:"required_unless=What column"`
|
|
Prefix string `json:"prefix"`
|
|
}
|
|
|
|
// filterCompleteHandlerOutput describes the output of the /filter/complete endpoint.
|
|
type filterCompleteHandlerOutput struct {
|
|
Completions []filterCompletion `json:"completions"`
|
|
}
|
|
type filterCompletion struct {
|
|
Label string `json:"label"`
|
|
Detail string `json:"detail,omitempty"`
|
|
Quoted bool `json:"quoted"` // should the return value be quoted?
|
|
}
|
|
|
|
func (c *Component) filterCompleteHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
var input filterCompleteHandlerInput
|
|
if err := gc.ShouldBindJSON(&input); err != nil {
|
|
gc.JSON(http.StatusBadRequest, gin.H{"message": helpers.Capitalize(err.Error())})
|
|
return
|
|
}
|
|
|
|
completions := []filterCompletion{}
|
|
switch input.What {
|
|
case "column":
|
|
_, err := filter.Parse("", []byte{}, filter.Entrypoint("ConditionExpr"))
|
|
if err != nil {
|
|
for _, candidate := range filter.Expected(err) {
|
|
if !strings.HasSuffix(candidate, `"i`) {
|
|
continue
|
|
}
|
|
candidate = candidate[1 : len(candidate)-2]
|
|
completions = append(completions, filterCompletion{
|
|
Label: candidate,
|
|
Detail: "column name",
|
|
})
|
|
}
|
|
}
|
|
case "operator":
|
|
_, err := filter.Parse("",
|
|
[]byte(fmt.Sprintf("%s ", input.Column)),
|
|
filter.Entrypoint("ConditionExpr"))
|
|
if err != nil {
|
|
for _, candidate := range filter.Expected(err) {
|
|
if !strings.HasPrefix(candidate, `"`) {
|
|
continue
|
|
}
|
|
candidate = strings.TrimSuffix(
|
|
strings.TrimSuffix(candidate[1:len(candidate)-1], `"i`),
|
|
`"`)
|
|
if candidate != "--" && candidate != "/*" {
|
|
if candidate == "IN" || candidate == "NOTIN" {
|
|
candidate = candidate + " ("
|
|
}
|
|
completions = append(completions, filterCompletion{
|
|
Label: candidate,
|
|
Detail: "comparison operator",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
case "value":
|
|
var column, detail string
|
|
switch strings.ToLower(input.Column) {
|
|
case "inifboundary", "outifboundary":
|
|
completions = append(completions, filterCompletion{
|
|
Label: "internal",
|
|
Detail: "network boundary",
|
|
}, filterCompletion{
|
|
Label: "external",
|
|
Detail: "network boundary",
|
|
}, filterCompletion{
|
|
Label: "undefined",
|
|
Detail: "network boundary",
|
|
})
|
|
case "etype":
|
|
completions = append(completions, filterCompletion{
|
|
Label: "IPv4",
|
|
Detail: "ethernet type",
|
|
}, filterCompletion{
|
|
Label: "IPv6",
|
|
Detail: "ethernet type",
|
|
})
|
|
case "proto":
|
|
// Do not complete from Clickhouse, we want a subset of options
|
|
completions = append(completions,
|
|
filterCompletion{"TCP", "protocol", true},
|
|
filterCompletion{"UDP", "protocol", true},
|
|
filterCompletion{"SCTP", "protocol", true},
|
|
filterCompletion{"ICMP", "protocol", true},
|
|
filterCompletion{"IPv6-ICMP", "protocol", true},
|
|
filterCompletion{"GRE", "protocol", true},
|
|
filterCompletion{"ESP", "protocol", true},
|
|
filterCompletion{"AH", "protocol", true},
|
|
filterCompletion{"IPIP", "protocol", true},
|
|
filterCompletion{"VRRP", "protocol", true},
|
|
filterCompletion{"L2TP", "protocol", true},
|
|
filterCompletion{"IGMP", "protocol", true},
|
|
filterCompletion{"PIM", "protocol", true},
|
|
filterCompletion{"IPv4", "protocol", true},
|
|
filterCompletion{"IPv6", "protocol", true})
|
|
case "srcas", "dstas":
|
|
results := []struct {
|
|
Label string `ch:"label"`
|
|
Detail string `ch:"detail"`
|
|
}{}
|
|
columnName := "SrcAS"
|
|
if strings.ToLower(input.Column) == "dstas" {
|
|
columnName = "DstAS"
|
|
}
|
|
sqlQuery := fmt.Sprintf(`
|
|
SELECT label, detail FROM (
|
|
SELECT concat('AS', toString(%s)) AS label, dictGet('asns', 'name', %s) AS detail, 1 AS rank
|
|
FROM flows
|
|
WHERE TimeReceived > date_sub(minute, 1, now())
|
|
AND detail != ''
|
|
AND positionCaseInsensitive(detail, $1) >= 1
|
|
GROUP BY %s
|
|
ORDER BY COUNT(*) DESC
|
|
LIMIT 20
|
|
UNION DISTINCT
|
|
SELECT concat('AS', toString(asn)) AS label, name AS detail, 2 AS rank
|
|
FROM asns
|
|
WHERE positionCaseInsensitive(name, $1) >= 1
|
|
ORDER BY positionCaseInsensitive(name, $1) ASC, asn ASC
|
|
LIMIT 20
|
|
) GROUP BY label, detail ORDER BY MIN(rank) ASC, MIN(rowNumberInBlock()) ASC LIMIT 20`,
|
|
columnName, columnName, columnName)
|
|
if err := c.d.ClickHouseDB.Conn.Select(ctx, &results, sqlQuery, input.Prefix); err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
break
|
|
}
|
|
for _, result := range results {
|
|
completions = append(completions, filterCompletion{
|
|
Label: result.Label,
|
|
Detail: result.Detail,
|
|
Quoted: false,
|
|
})
|
|
}
|
|
input.Prefix = "" // We have handled this internally
|
|
case "srcnetname", "dstnetname":
|
|
results := []struct {
|
|
Name string `ch:"name"`
|
|
}{}
|
|
if err := c.d.ClickHouseDB.Conn.Select(ctx, &results, `
|
|
SELECT DISTINCT name
|
|
FROM networks
|
|
WHERE positionCaseInsensitive(name, $1) >= 1
|
|
ORDER BY name
|
|
LIMIT 20`, input.Prefix); err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
break
|
|
}
|
|
for _, result := range results {
|
|
completions = append(completions, filterCompletion{
|
|
Label: result.Name,
|
|
Detail: "network name",
|
|
Quoted: true,
|
|
})
|
|
}
|
|
input.Prefix = ""
|
|
case "exportername":
|
|
column = "ExporterName"
|
|
detail = "exporter name"
|
|
case "exportergroup":
|
|
column = "ExporterGroup"
|
|
detail = "exporter group"
|
|
case "inifname", "outifname":
|
|
column = "IfName"
|
|
detail = "interface name"
|
|
case "inifdescription", "outifdescription":
|
|
column = "IfDescription"
|
|
detail = "interface description"
|
|
case "inifconnectivity", "outifconnectivity":
|
|
column = "IfConnectivity"
|
|
detail = "connectivity type"
|
|
case "inifprovider", "outifprovider":
|
|
column = "IfProvider"
|
|
detail = "provider name"
|
|
}
|
|
if column != "" {
|
|
// Query "exporter" table
|
|
sqlQuery := fmt.Sprintf(`
|
|
SELECT %s AS label
|
|
FROM exporters
|
|
WHERE positionCaseInsensitive(%s, $1) >= 1
|
|
GROUP BY %s
|
|
ORDER BY positionCaseInsensitive(%s, $1) ASC, %s ASC
|
|
LIMIT 20`, column, column, column, column, column)
|
|
results := []struct {
|
|
Label string `ch:"label"`
|
|
}{}
|
|
if err := c.d.ClickHouseDB.Conn.Select(ctx, &results, sqlQuery, input.Prefix); err != nil {
|
|
c.r.Err(err).Msg("unable to query database")
|
|
break
|
|
}
|
|
for _, result := range results {
|
|
completions = append(completions, filterCompletion{
|
|
Label: result.Label,
|
|
Detail: detail,
|
|
Quoted: true,
|
|
})
|
|
}
|
|
input.Prefix = ""
|
|
}
|
|
}
|
|
filteredCompletions := []filterCompletion{}
|
|
for _, completion := range completions {
|
|
if strings.HasPrefix(strings.ToLower(completion.Label), strings.ToLower(input.Prefix)) {
|
|
filteredCompletions = append(filteredCompletions, completion)
|
|
}
|
|
}
|
|
gc.JSON(http.StatusOK, filterCompleteHandlerOutput{filteredCompletions})
|
|
return
|
|
}
|
|
|
|
func (c *Component) filterSavedListHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
user := gc.MustGet("user").(authentication.UserInformation).Login
|
|
filters, err := c.d.Database.ListSavedFilters(ctx, user)
|
|
if err != nil {
|
|
c.r.Err(err).Msg("unable to list filters")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "unable to list filters"})
|
|
return
|
|
}
|
|
gc.JSON(http.StatusOK, gin.H{"filters": filters})
|
|
}
|
|
|
|
func (c *Component) filterSavedDeleteHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
user := gc.MustGet("user").(authentication.UserInformation).Login
|
|
id, err := strconv.ParseUint(gc.Param("id"), 10, 64)
|
|
if err != nil {
|
|
gc.JSON(http.StatusBadRequest, gin.H{"message": "bad ID format"})
|
|
return
|
|
}
|
|
if err := c.d.Database.DeleteSavedFilter(ctx, database.SavedFilter{
|
|
ID: uint(id),
|
|
User: user,
|
|
}); err != nil {
|
|
// Assume this is because it is not found
|
|
gc.JSON(http.StatusNotFound, gin.H{"message": "filter not found"})
|
|
return
|
|
}
|
|
gc.JSON(http.StatusNoContent, nil)
|
|
}
|
|
|
|
func (c *Component) filterSavedAddHandlerFunc(gc *gin.Context) {
|
|
ctx := c.t.Context(gc.Request.Context())
|
|
user := gc.MustGet("user").(authentication.UserInformation).Login
|
|
var filter database.SavedFilter
|
|
if err := gc.ShouldBindJSON(&filter); err != nil {
|
|
gc.JSON(http.StatusBadRequest, gin.H{"message": helpers.Capitalize(err.Error())})
|
|
return
|
|
}
|
|
filter.User = user
|
|
if err := c.d.Database.CreateSavedFilter(ctx, filter); err != nil {
|
|
c.r.Err(err).Msg("cannot create saved filter")
|
|
gc.JSON(http.StatusInternalServerError, gin.H{"message": "cannot create new filter"})
|
|
return
|
|
}
|
|
gc.JSON(http.StatusNoContent, nil)
|
|
}
|