Files
akvorado/console/filter.go
Vincent Bernat 8be1bca4fd license: AGPL-3.0-only
```
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'
```
2022-06-29 11:42:28 +02:00

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)
}