mirror of
https://github.com/activecm/rita
synced 2026-06-08 13:02:45 +00:00
Fixed viewer issues, fixed and added tests to cmd and util packages
Co-Authored-By: Naomi Kramer <naomiagoddard@gmail.com>
This commit is contained in:
+10
-8
@@ -15,6 +15,9 @@ import (
|
||||
var ErrMissingDatabaseName = errors.New("database name is required")
|
||||
var ErrMissingConfigPath = errors.New("config path parameter is required")
|
||||
var ErrTooManyArguments = errors.New("too many arguments provided")
|
||||
var ErrInvalidConfigObject = errors.New("config was nil or invalid")
|
||||
var ErrCurrentVersionEmpty = errors.New("current version unset")
|
||||
var ErrCheckingForUpdate = errors.New("error checking for newer version of RITA")
|
||||
|
||||
func Commands() []*cli.Command {
|
||||
return []*cli.Command{
|
||||
@@ -39,21 +42,20 @@ func ConfigFlag(required bool) *cli.StringFlag {
|
||||
}
|
||||
}
|
||||
|
||||
func CheckForUpdate(cCtx *cli.Context, afs afero.Fs) error {
|
||||
func CheckForUpdate(cfg *config.Config) error {
|
||||
// make sure config is not nil
|
||||
if cfg == nil {
|
||||
return ErrInvalidConfigObject
|
||||
}
|
||||
|
||||
// get the current version
|
||||
currentVersion := config.Version
|
||||
|
||||
// load config file
|
||||
cfg, err := config.ReadFileConfig(afs, cCtx.String("config"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading config file: %w", err)
|
||||
}
|
||||
|
||||
// check for update if version is set
|
||||
if cfg.UpdateCheckEnabled && currentVersion != "" && currentVersion != "dev" {
|
||||
newer, latestVersion, err := util.CheckForNewerVersion(github.NewClient(nil), currentVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error checking for newer version of RITA: %w", err)
|
||||
return fmt.Errorf("%w: %w", ErrCheckingForUpdate, err)
|
||||
}
|
||||
if newer {
|
||||
fmt.Printf("\n\t✨ A newer version (%s) of RITA is available! https://github.com/activecm/rita/releases ✨\n\n", latestVersion)
|
||||
|
||||
+116
-15
@@ -1,6 +1,7 @@
|
||||
package cmd_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -8,11 +9,15 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/activecm/rita/v5/cmd"
|
||||
"github.com/activecm/rita/v5/config"
|
||||
"github.com/activecm/rita/v5/database"
|
||||
"github.com/activecm/rita/v5/util"
|
||||
"github.com/google/go-github/github"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
@@ -91,20 +96,7 @@ func (c *CmdTestSuite) TearDownSuite() {
|
||||
// func (d *DatabaseTestSuite) TearDownTest() {}
|
||||
|
||||
// SetupSubTest is run before each subtest
|
||||
func (c *CmdTestSuite) SetupSubTest() {
|
||||
t := c.T()
|
||||
fmt.Println("Running setup subtest...")
|
||||
|
||||
// drop all databases that may have been created during subtest
|
||||
if c.server != nil && c.server.Conn != nil {
|
||||
dbs, err := c.server.ListImportDatabases()
|
||||
require.NoError(t, err, "listing databases should not produce an error")
|
||||
for _, db := range dbs {
|
||||
err := c.server.DeleteSensorDB(db.Name)
|
||||
require.NoError(t, err, "dropping database should not produce an error")
|
||||
}
|
||||
}
|
||||
}
|
||||
// func (c *CmdTestSuite) SetupSubTest() {}
|
||||
|
||||
// TearDownSubTest is run after each subtest
|
||||
// func (c *CmdTestSuite) TearDownSubTest() {}
|
||||
@@ -148,8 +140,117 @@ func setupTestApp(commands []*cli.Command, flags []cli.Flag) (*cli.App, context.
|
||||
// this prevents the test from exiting when testing for errors
|
||||
app.ExitErrHandler = func(_ *cli.Context, _ error) {
|
||||
// add any custom test logic, or assertions or leave it blank
|
||||
|
||||
}
|
||||
|
||||
return app, ctx
|
||||
}
|
||||
|
||||
func TestCheckForUpdate(t *testing.T) {
|
||||
// set up file system interface
|
||||
afs := afero.NewOsFs()
|
||||
|
||||
// load the config file
|
||||
cfg, err := config.ReadFileConfig(afs, ConfigPath)
|
||||
require.NoError(t, err, "config should load without error")
|
||||
|
||||
// get latest release version
|
||||
latestVersion, err := util.GetLatestReleaseVersion(github.NewClient(nil), "activecm", "rita")
|
||||
require.NoError(t, err, "latest release version should be retrieved without error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg *config.Config
|
||||
updateCheckEnabled bool
|
||||
currentVersion string
|
||||
expectedErr error
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "New version available",
|
||||
updateCheckEnabled: true,
|
||||
cfg: cfg,
|
||||
currentVersion: "v0.0.0",
|
||||
expectedOutput: fmt.Sprintf("\n\t✨ A newer version (%s) of RITA is available! https://github.com/activecm/rita/releases ✨\n\n", latestVersion),
|
||||
},
|
||||
{
|
||||
name: "Error checking for newer version",
|
||||
updateCheckEnabled: true,
|
||||
cfg: cfg,
|
||||
currentVersion: "notaversion",
|
||||
expectedErr: cmd.ErrCheckingForUpdate,
|
||||
},
|
||||
{
|
||||
name: "Update check disabled",
|
||||
updateCheckEnabled: false,
|
||||
cfg: cfg,
|
||||
currentVersion: "1.0.0",
|
||||
},
|
||||
{
|
||||
name: "Current version is dev",
|
||||
updateCheckEnabled: true,
|
||||
cfg: cfg,
|
||||
currentVersion: "dev",
|
||||
},
|
||||
{
|
||||
name: "Current version is empty",
|
||||
updateCheckEnabled: true,
|
||||
cfg: cfg,
|
||||
currentVersion: "",
|
||||
},
|
||||
{
|
||||
name: "Nil config",
|
||||
cfg: nil,
|
||||
expectedErr: cmd.ErrInvalidConfigObject,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// set update check enabled in config
|
||||
if test.cfg != nil {
|
||||
test.cfg.UpdateCheckEnabled = test.updateCheckEnabled
|
||||
}
|
||||
|
||||
// override global variables and functions
|
||||
config.Version = test.currentVersion
|
||||
|
||||
// capture stdout
|
||||
output := captureOutput(t, func() {
|
||||
err := cmd.CheckForUpdate(test.cfg)
|
||||
// check error
|
||||
if test.expectedErr != nil {
|
||||
require.Contains(t, err.Error(), test.expectedErr.Error(), "error should contain expected value")
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
|
||||
// Assert output
|
||||
if test.expectedOutput != "" {
|
||||
assert.Equal(t, test.expectedOutput, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// captureOutput captures stdout from a function
|
||||
func captureOutput(t *testing.T, f func()) string {
|
||||
t.Helper()
|
||||
|
||||
// capture stdout
|
||||
old := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
os.Stdout = w
|
||||
|
||||
// run the function
|
||||
f()
|
||||
|
||||
// close and restore stdout
|
||||
w.Close()
|
||||
os.Stdout = old
|
||||
var buf bytes.Buffer
|
||||
_, err = buf.ReadFrom(r)
|
||||
require.NoError(t, err)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ var DeleteCommand = &cli.Command{
|
||||
}
|
||||
|
||||
// check for updates after running the command
|
||||
if err := CheckForUpdate(cCtx, afero.NewOsFs()); err != nil {
|
||||
if err := CheckForUpdate(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -202,8 +202,12 @@ func (c *CmdTestSuite) TestRunDeleteCmd() {
|
||||
}
|
||||
|
||||
// validate that the expected databases remain
|
||||
for _, db := range test.expectedRemainingDbs {
|
||||
require.Contains(t, dbString, db, "database %s should not have been deleted", db)
|
||||
require.ElementsMatch(t, test.expectedRemainingDbs, dbString, "remaining databases should match expected value")
|
||||
|
||||
// cleanup
|
||||
for _, db := range test.dbs {
|
||||
err := c.server.DeleteSensorDB(db.name)
|
||||
require.NoError(t, err, "dropping database should not produce an error")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
+10
-4
@@ -40,6 +40,7 @@ var ErrInvalidLogHourRange = errors.New("could not parse hour from log file name
|
||||
var ErrInvalidLogType = errors.New("incompatible log type")
|
||||
var ErrIncompatibleFileExtension = errors.New("incompatible file extension")
|
||||
var ErrSkippedDuplicateLog = errors.New("encountered file with same name but different extension, skipping file due to older last modified time")
|
||||
var ErrMissingLogDirectory = errors.New("log directory flag is required")
|
||||
|
||||
type WalkError struct {
|
||||
Path string
|
||||
@@ -110,7 +111,7 @@ var ImportCommand = &cli.Command{
|
||||
}
|
||||
|
||||
// check for updates after running the command
|
||||
if err := CheckForUpdate(cCtx, afero.NewOsFs()); err != nil {
|
||||
if err := CheckForUpdate(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -303,7 +304,7 @@ func RunImportCmd(startTime time.Time, cfg *config.Config, afs afero.Fs, logDir
|
||||
|
||||
func ValidateLogDirectory(afs afero.Fs, logDir string) error {
|
||||
if logDir == "" {
|
||||
return fmt.Errorf("log directory flag is required")
|
||||
return ErrMissingLogDirectory
|
||||
}
|
||||
|
||||
dir, err := util.ParseRelativePath(logDir)
|
||||
@@ -343,7 +344,12 @@ func ValidateDatabaseName(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFolderDate(folder string) (time.Time, error) {
|
||||
// ParseFolderDate extracts the date from a given folder name
|
||||
func ParseFolderDate(folder string) (time.Time, error) {
|
||||
if folder == "" {
|
||||
return time.Unix(0, 0), errors.New("folder name cannot be empty")
|
||||
}
|
||||
|
||||
// check if the path is a directory
|
||||
folderDate, err := time.Parse(time.DateOnly, folder)
|
||||
if err != nil {
|
||||
@@ -489,7 +495,7 @@ func WalkFiles(afs afero.Fs, root string) ([]HourlyZeekLogs, []WalkError, error)
|
||||
}
|
||||
|
||||
parentDir := filepath.Base(filepath.Dir(file.path))
|
||||
folderDate, err := parseFolderDate(parentDir)
|
||||
folderDate, err := ParseFolderDate(parentDir)
|
||||
if err != nil {
|
||||
walkErrors = append(walkErrors, WalkError{Path: path, Error: err})
|
||||
}
|
||||
|
||||
+384
-109
@@ -52,56 +52,193 @@ NON-ROLLING LOGS
|
||||
*/
|
||||
|
||||
func (c *CmdTestSuite) TestRunImportCmd() {
|
||||
|
||||
type TestCase struct {
|
||||
type importDB struct {
|
||||
name string
|
||||
afs afero.Fs
|
||||
dbName string
|
||||
rolling bool
|
||||
rebuild bool
|
||||
logDir string
|
||||
hours [][]string
|
||||
rolling bool
|
||||
rebuild bool
|
||||
expectedImport int
|
||||
expectedError error
|
||||
}
|
||||
|
||||
type TestCase struct {
|
||||
name string
|
||||
afs afero.Fs
|
||||
importDBs []importDB
|
||||
// dbName string
|
||||
// rolling bool
|
||||
// rebuild bool
|
||||
// logDir string
|
||||
// hours [][]string
|
||||
// expectedImport int
|
||||
// expectedError error
|
||||
}
|
||||
|
||||
testCases := []TestCase{
|
||||
{
|
||||
name: "No Subdirectories, No Hours",
|
||||
afs: afero.NewOsFs(),
|
||||
dbName: "ahhhhhhhhhh",
|
||||
rolling: false,
|
||||
rebuild: false,
|
||||
logDir: "../test_data/valid_tsv",
|
||||
hours: [][]string{{"conn.log.gz", "dns.log.gz", "http.log.gz", "ssl.log.gz", "open_conn.log.gz", "open_http.log.gz", "open_ssl.log.gz"}},
|
||||
expectedImport: 1,
|
||||
name: "No Subdirectories, No Hours",
|
||||
afs: afero.NewOsFs(),
|
||||
importDBs: []importDB{
|
||||
{
|
||||
name: "ahhhhhhhhhh",
|
||||
logDir: "../test_data/valid_tsv",
|
||||
hours: [][]string{{"conn.log.gz", "dns.log.gz", "http.log.gz", "ssl.log.gz", "open_conn.log.gz", "open_http.log.gz", "open_ssl.log.gz"}},
|
||||
rolling: false,
|
||||
rebuild: false,
|
||||
expectedImport: 1,
|
||||
expectedError: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Simple, SubDirectories - Multi-Day Logs",
|
||||
afs: afero.NewMemMapFs(),
|
||||
dbName: "bingbong",
|
||||
rolling: false,
|
||||
rebuild: false,
|
||||
logDir: "/logs",
|
||||
hours: [][]string{
|
||||
{"2024-04-29/conn.log", "2024-04-29/dns.log", "2024-04-29/http.log", "2024-04-29/ssl.log", "2024-04-29/open_conn.log", "2024-04-29/open_http.log", "2024-04-29/open_ssl.log"},
|
||||
{"2024-05-01/conn.log", "2024-05-01/dns.log", "2024-05-01/http.log", "2024-05-01/ssl.log", "2024-05-01/open_conn.log", "2024-05-01/open_http.log", "2024-05-01/open_ssl.log", "2024-05-01/ssl_blue.log"},
|
||||
name: "Simple, SubDirectories - Multi-Day Logs",
|
||||
afs: afero.NewMemMapFs(),
|
||||
importDBs: []importDB{
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs",
|
||||
hours: [][]string{
|
||||
{"2024-04-29/conn.log", "2024-04-29/dns.log", "2024-04-29/http.log", "2024-04-29/ssl.log", "2024-04-29/open_conn.log", "2024-04-29/open_http.log", "2024-04-29/open_ssl.log"},
|
||||
{"2024-05-01/conn.log", "2024-05-01/dns.log", "2024-05-01/http.log", "2024-05-01/ssl.log", "2024-05-01/open_conn.log", "2024-05-01/open_http.log", "2024-05-01/open_ssl.log", "2024-05-01/ssl_blue.log"},
|
||||
},
|
||||
rolling: false,
|
||||
rebuild: false,
|
||||
expectedImport: 2,
|
||||
expectedError: nil,
|
||||
},
|
||||
},
|
||||
expectedImport: 2,
|
||||
},
|
||||
{
|
||||
name: "SubDirectories, Multi-Day, Multi-Hour Logs",
|
||||
afs: afero.NewMemMapFs(),
|
||||
dbName: "bingbong",
|
||||
rolling: false,
|
||||
rebuild: false,
|
||||
logDir: "/logs",
|
||||
hours: [][]string{
|
||||
{"2024-04-29/conn.00:00:00-01:00:00.log", "2024-04-29/open_conn.00:00:00-01:00:00.log", "2024-04-29/dns.00:00:00-01:00:00.log", "2024-04-29/http.00:00:00-01:00:00.log", "2024-04-29/open_http.00:00:00-01:00:00.log", "2024-04-29/ssl.00:00:00-01:00:00.log", "2024-04-29/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-04-29/conn.23:00:00-00:00:00.log", "2024-04-29/open_conn.23:00:00-00:00:00.log", "2024-04-29/dns.23:00:00-00:00:00.log", "2024-04-29/http.23:00:00-00:00:00.log", "2024-04-29/open_http.23:00:00-00:00:00.log", "2024-04-29/ssl.23:00:00-00:00:00.log", "2024-04-29/open_ssl.23:00:00-00:00:00.log"},
|
||||
{"2024-05-01/conn.00:00:00-01:00:00.log", "2024-05-01/open_conn.00:00:00-01:00:00.log", "2024-05-01/dns.00:00:00-01:00:00.log", "2024-05-01/http.00:00:00-01:00:00.log", "2024-05-01/open_http.00:00:00-01:00:00.log", "2024-05-01/ssl.00:00:00-01:00:00.log", "2024-05-01/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-05-01/conn.23:00:00-00:00:00.log", "2024-05-01/open_conn.23:00:00-00:00:00.log", "2024-05-01/dns.23:00:00-00:00:00.log", "2024-05-01/http.23:00:00-00:00:00.log", "2024-05-01/open_http.23:00:00-00:00:00.log", "2024-05-01/ssl.23:00:00-00:00:00.log", "2024-05-01/open_ssl.23:00:00-00:00:00.log", "2024-05-01/ssl_blue.23:00:00-00:00:00.log"},
|
||||
name: "SubDirectories, Multi-Day, Multi-Hour Logs",
|
||||
afs: afero.NewMemMapFs(),
|
||||
importDBs: []importDB{
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs",
|
||||
hours: [][]string{
|
||||
{"2024-04-29/conn.00:00:00-01:00:00.log", "2024-04-29/open_conn.00:00:00-01:00:00.log", "2024-04-29/dns.00:00:00-01:00:00.log", "2024-04-29/http.00:00:00-01:00:00.log", "2024-04-29/open_http.00:00:00-01:00:00.log", "2024-04-29/ssl.00:00:00-01:00:00.log", "2024-04-29/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-04-29/conn.23:00:00-00:00:00.log", "2024-04-29/open_conn.23:00:00-00:00:00.log", "2024-04-29/dns.23:00:00-00:00:00.log", "2024-04-29/http.23:00:00-00:00:00.log", "2024-04-29/open_http.23:00:00-00:00:00.log", "2024-04-29/ssl.23:00:00-00:00:00.log", "2024-04-29/open_ssl.23:00:00-00:00:00.log"},
|
||||
{"2024-05-01/conn.00:00:00-01:00:00.log", "2024-05-01/open_conn.00:00:00-01:00:00.log", "2024-05-01/dns.00:00:00-01:00:00.log", "2024-05-01/http.00:00:00-01:00:00.log", "2024-05-01/open_http.00:00:00-01:00:00.log", "2024-05-01/ssl.00:00:00-01:00:00.log", "2024-05-01/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-05-01/conn.23:00:00-00:00:00.log", "2024-05-01/open_conn.23:00:00-00:00:00.log", "2024-05-01/dns.23:00:00-00:00:00.log", "2024-05-01/http.23:00:00-00:00:00.log", "2024-05-01/open_http.23:00:00-00:00:00.log", "2024-05-01/ssl.23:00:00-00:00:00.log", "2024-05-01/open_ssl.23:00:00-00:00:00.log", "2024-05-01/ssl_blue.23:00:00-00:00:00.log"},
|
||||
},
|
||||
rolling: false,
|
||||
rebuild: false,
|
||||
expectedImport: 4,
|
||||
expectedError: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "SubDirectories, Multi-Day, Multi-Hour Logs",
|
||||
afs: afero.NewMemMapFs(),
|
||||
importDBs: []importDB{
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs",
|
||||
hours: [][]string{
|
||||
{"2024-04-29/conn.00:00:00-01:00:00.log", "2024-04-29/open_conn.00:00:00-01:00:00.log", "2024-04-29/dns.00:00:00-01:00:00.log", "2024-04-29/http.00:00:00-01:00:00.log", "2024-04-29/open_http.00:00:00-01:00:00.log", "2024-04-29/ssl.00:00:00-01:00:00.log", "2024-04-29/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-04-29/conn.23:00:00-00:00:00.log", "2024-04-29/open_conn.23:00:00-00:00:00.log", "2024-04-29/dns.23:00:00-00:00:00.log", "2024-04-29/http.23:00:00-00:00:00.log", "2024-04-29/open_http.23:00:00-00:00:00.log", "2024-04-29/ssl.23:00:00-00:00:00.log", "2024-04-29/open_ssl.23:00:00-00:00:00.log"},
|
||||
{"2024-05-01/conn.00:00:00-01:00:00.log", "2024-05-01/open_conn.00:00:00-01:00:00.log", "2024-05-01/dns.00:00:00-01:00:00.log", "2024-05-01/http.00:00:00-01:00:00.log", "2024-05-01/open_http.00:00:00-01:00:00.log", "2024-05-01/ssl.00:00:00-01:00:00.log", "2024-05-01/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-05-01/conn.23:00:00-00:00:00.log", "2024-05-01/open_conn.23:00:00-00:00:00.log", "2024-05-01/dns.23:00:00-00:00:00.log", "2024-05-01/http.23:00:00-00:00:00.log", "2024-05-01/open_http.23:00:00-00:00:00.log", "2024-05-01/ssl.23:00:00-00:00:00.log", "2024-05-01/open_ssl.23:00:00-00:00:00.log", "2024-05-01/ssl_blue.23:00:00-00:00:00.log"},
|
||||
},
|
||||
rolling: false,
|
||||
rebuild: false,
|
||||
expectedImport: 4,
|
||||
expectedError: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Rolling",
|
||||
afs: afero.NewMemMapFs(),
|
||||
importDBs: []importDB{
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs/1",
|
||||
hours: [][]string{
|
||||
{"conn.log", "dns.log", "http.log", "ssl.log", "open_conn.log", "open_http.log", "open_ssl.log"},
|
||||
},
|
||||
rolling: true,
|
||||
rebuild: false,
|
||||
expectedImport: 1,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs/2",
|
||||
hours: [][]string{
|
||||
{"conn.log", "dns.log", "http.log", "ssl.log", "open_conn.log", "open_http.log", "open_ssl.log"},
|
||||
},
|
||||
rolling: true,
|
||||
rebuild: false,
|
||||
expectedImport: 1,
|
||||
expectedError: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Rolling - Multi-Day, Multi-Hour Logs",
|
||||
afs: afero.NewMemMapFs(),
|
||||
importDBs: []importDB{
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs/1",
|
||||
hours: [][]string{
|
||||
{"2024-04-29/conn.00:00:00-01:00:00.log", "2024-04-29/open_conn.00:00:00-01:00:00.log", "2024-04-29/dns.00:00:00-01:00:00.log", "2024-04-29/http.00:00:00-01:00:00.log", "2024-04-29/open_http.00:00:00-01:00:00.log", "2024-04-29/ssl.00:00:00-01:00:00.log", "2024-04-29/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-04-29/conn.23:00:00-00:00:00.log", "2024-04-29/open_conn.23:00:00-00:00:00.log", "2024-04-29/dns.23:00:00-00:00:00.log", "2024-04-29/http.23:00:00-00:00:00.log", "2024-04-29/open_http.23:00:00-00:00:00.log", "2024-04-29/ssl.23:00:00-00:00:00.log", "2024-04-29/open_ssl.23:00:00-00:00:00.log"},
|
||||
{"2024-05-01/conn.00:00:00-01:00:00.log", "2024-05-01/open_conn.00:00:00-01:00:00.log", "2024-05-01/dns.00:00:00-01:00:00.log", "2024-05-01/http.00:00:00-01:00:00.log", "2024-05-01/open_http.00:00:00-01:00:00.log", "2024-05-01/ssl.00:00:00-01:00:00.log", "2024-05-01/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-05-01/conn.23:00:00-00:00:00.log", "2024-05-01/open_conn.23:00:00-00:00:00.log", "2024-05-01/dns.23:00:00-00:00:00.log", "2024-05-01/http.23:00:00-00:00:00.log", "2024-05-01/open_http.23:00:00-00:00:00.log", "2024-05-01/ssl.23:00:00-00:00:00.log", "2024-05-01/open_ssl.23:00:00-00:00:00.log", "2024-05-01/ssl_blue.23:00:00-00:00:00.log"},
|
||||
},
|
||||
rolling: true,
|
||||
rebuild: false,
|
||||
expectedImport: 4,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs/2",
|
||||
hours: [][]string{
|
||||
{"2024-05-02/conn.00:00:00-01:00:00.log", "2024-05-02/open_conn.00:00:00-01:00:00.log", "2024-05-02/dns.00:00:00-01:00:00.log", "2024-05-02/http.00:00:00-01:00:00.log", "2024-05-02/open_http.00:00:00-01:00:00.log", "2024-05-02/ssl.00:00:00-01:00:00.log", "2024-05-02/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-05-02/conn.23:00:00-00:00:00.log", "2024-05-02/open_conn.23:00:00-00:00:00.log", "2024-05-02/dns.23:00:00-00:00:00.log", "2024-05-02/http.23:00:00-00:00:00.log", "2024-05-02/open_http.23:00:00-00:00:00.log", "2024-05-02/ssl.23:00:00-00:00:00.log", "2024-05-02/open_ssl.23:00:00-00:00:00.log"},
|
||||
{"2024-05-03/conn.00:00:00-01:00:00.log", "2024-05-03/open_conn.00:00:00-01:00:00.log", "2024-05-03/dns.00:00:00-01:00:00.log", "2024-05-03/http.00:00:00-01:00:00.log", "2024-05-03/open_http.00:00:00-01:00:00.log", "2024-05-03/ssl.00:00:00-01:00:00.log", "2024-05-03/open_ssl.00:00:00-01:00:00.log"},
|
||||
{"2024-05-03/conn.23:00:00-00:00:00.log", "2024-05-03/open_conn.23:00:00-00:00:00.log", "2024-05-03/dns.23:00:00-00:00:00.log", "2024-05-03/http.23:00:00-00:00:00.log", "2024-05-03/open_http.23:00:00-00:00:00.log", "2024-05-03/ssl.23:00:00-00:00:00.log", "2024-05-03/open_ssl.23:00:00-00:00:00.log", "2024-05-03/ssl_blue.23:00:00-00:00:00.log"},
|
||||
},
|
||||
rolling: true,
|
||||
rebuild: false,
|
||||
expectedImport: 4,
|
||||
expectedError: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Files Previously Imported",
|
||||
afs: afero.NewMemMapFs(),
|
||||
importDBs: []importDB{
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs/1",
|
||||
hours: [][]string{
|
||||
{"conn.log", "dns.log", "http.log", "ssl.log", "open_conn.log", "open_http.log", "open_ssl.log"},
|
||||
},
|
||||
rolling: true,
|
||||
rebuild: false,
|
||||
expectedImport: 1,
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "bingbong",
|
||||
logDir: "/logs/1",
|
||||
hours: [][]string{
|
||||
{"conn.log", "dns.log", "http.log", "ssl.log", "open_conn.log", "open_http.log", "open_ssl.log"},
|
||||
},
|
||||
rolling: true,
|
||||
rebuild: false,
|
||||
expectedImport: 1,
|
||||
expectedError: importer.ErrAllFilesPreviouslyImported,
|
||||
},
|
||||
},
|
||||
expectedImport: 4,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -109,88 +246,111 @@ func (c *CmdTestSuite) TestRunImportCmd() {
|
||||
c.Run(tc.name, func() {
|
||||
t := c.T()
|
||||
|
||||
importStartedAt := time.Now()
|
||||
// loop over each importDB
|
||||
for _, db := range tc.importDBs {
|
||||
// get start time
|
||||
importStartedAt := time.Now()
|
||||
|
||||
var files []string
|
||||
var fullPathHours [][]string
|
||||
var files []string
|
||||
var fullPathHours [][]string
|
||||
|
||||
// get the root directory path
|
||||
fullRootDir := tc.logDir
|
||||
// get the root directory path
|
||||
fullRootDir := db.logDir
|
||||
|
||||
// if we are using the real logs directory, we need to get the real full path
|
||||
if tc.logDir != "/logs" {
|
||||
// get the current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting current working directory:", err)
|
||||
return
|
||||
// if we are using the real logs directory, we need to get the real full path
|
||||
if !strings.HasPrefix(db.logDir, "/logs") {
|
||||
// get the current working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Println("Error getting current working directory:", err)
|
||||
return
|
||||
}
|
||||
|
||||
fullRootDir = filepath.Join(cwd, db.logDir)
|
||||
}
|
||||
|
||||
fullRootDir = filepath.Join(cwd, tc.logDir)
|
||||
}
|
||||
// iterate over each day of logs
|
||||
for _, day := range db.hours {
|
||||
// append all the files to a single list for creation
|
||||
files = append(files, day...)
|
||||
|
||||
// iterate over each day of logs
|
||||
for _, day := range tc.hours {
|
||||
// append all the files to a single list for creation
|
||||
files = append(files, day...)
|
||||
|
||||
// convert the day list of files to full path versions
|
||||
var fullHourFiles []string
|
||||
for _, file := range day {
|
||||
fullPath := filepath.Join(fullRootDir, file)
|
||||
fullHourFiles = append(fullHourFiles, fullPath)
|
||||
}
|
||||
fullPathHours = append(fullPathHours, fullHourFiles)
|
||||
}
|
||||
|
||||
// if we are using the mock directory, we need to create it along with the files
|
||||
if tc.logDir == "/logs" {
|
||||
// create mock directory with files
|
||||
createMockZeekLogs(t, tc.afs, tc.logDir, files, true)
|
||||
}
|
||||
|
||||
// run the import command
|
||||
importResults, err := cmd.RunImportCmd(importStartedAt, c.cfg, tc.afs, tc.logDir, tc.dbName, tc.rolling, tc.rebuild)
|
||||
require.NoError(t, err, "running import command should not produce an error")
|
||||
require.NotNil(t, importResults, "import results should not be nil")
|
||||
|
||||
// verify the number of import IDs
|
||||
require.Len(t, importResults.ImportID, tc.expectedImport, "import results should have expected number of import IDs")
|
||||
|
||||
// check if the database exists
|
||||
exists, err := database.SensorDatabaseExists(context.Background(), c.server.Conn, tc.dbName)
|
||||
require.NoError(t, err, "checking if sensor database exists should not produce an error")
|
||||
require.True(t, exists, "sensor database should exist")
|
||||
|
||||
// check rolling status
|
||||
isRolling, err := database.GetRollingStatus(context.Background(), c.server.Conn, tc.dbName)
|
||||
require.NoError(t, err, "checking if sensor database is rolling should not produce an error")
|
||||
require.Equal(t, tc.rolling, isRolling, "rolling status should match expected value")
|
||||
|
||||
// verify imported paths for each hour
|
||||
for i := range fullPathHours {
|
||||
var result struct {
|
||||
Paths []string `ch:"paths"`
|
||||
// convert the day list of files to full path versions
|
||||
var fullHourFiles []string
|
||||
for _, file := range day {
|
||||
fullPath := filepath.Join(fullRootDir, file)
|
||||
fullHourFiles = append(fullHourFiles, fullPath)
|
||||
}
|
||||
fullPathHours = append(fullPathHours, fullHourFiles)
|
||||
}
|
||||
|
||||
ctx := clickhouse.Context(context.Background(), clickhouse.WithParameters(clickhouse.Parameters{
|
||||
"import_id": importResults.ImportID[i].Hex(),
|
||||
"database": tc.dbName,
|
||||
}))
|
||||
// if we are using the mock directory, we need to create it along with the files
|
||||
if strings.HasPrefix(db.logDir, "/logs") {
|
||||
// create mock directory with files
|
||||
createMockZeekLogs(t, tc.afs, db.logDir, files, true)
|
||||
}
|
||||
|
||||
err = c.server.Conn.QueryRow(ctx, `
|
||||
// run the import command
|
||||
importResults, err := cmd.RunImportCmd(importStartedAt, c.cfg, tc.afs, db.logDir, db.name, db.rolling, db.rebuild)
|
||||
|
||||
// check if we expect an error
|
||||
if db.expectedError != nil {
|
||||
require.Error(t, err, "running import command should produce an error")
|
||||
require.Contains(t, err.Error(), db.expectedError.Error(), "error should contain expected value")
|
||||
continue
|
||||
}
|
||||
|
||||
// if no error was expected, continue with the rest of the checks
|
||||
require.NoError(t, err, "running import command should not produce an error")
|
||||
require.NotNil(t, importResults, "import results should not be nil")
|
||||
|
||||
// verify the number of import IDs
|
||||
require.Len(t, importResults.ImportID, db.expectedImport, "import results should have expected number of import IDs")
|
||||
|
||||
// check if the database exists
|
||||
exists, err := database.SensorDatabaseExists(context.Background(), c.server.Conn, db.name)
|
||||
require.NoError(t, err, "checking if sensor database exists should not produce an error")
|
||||
require.True(t, exists, "sensor database should exist")
|
||||
|
||||
// check rolling status
|
||||
isRolling, err := database.GetRollingStatus(context.Background(), c.server.Conn, db.name)
|
||||
require.NoError(t, err, "checking if sensor database is rolling should not produce an error")
|
||||
require.Equal(t, db.rolling, isRolling, "rolling status should match expected value")
|
||||
|
||||
// verify imported paths for each hour
|
||||
for i := range fullPathHours {
|
||||
var result struct {
|
||||
Paths []string `ch:"paths"`
|
||||
}
|
||||
|
||||
ctx := clickhouse.Context(context.Background(), clickhouse.WithParameters(clickhouse.Parameters{
|
||||
"import_id": importResults.ImportID[i].Hex(),
|
||||
"database": db.name,
|
||||
}))
|
||||
|
||||
err = c.server.Conn.QueryRow(ctx, `
|
||||
SELECT groupArray(path) AS paths
|
||||
FROM metadatabase.files
|
||||
WHERE import_id = unhex({import_id:String}) AND database = {database:String}
|
||||
`).ScanStruct(&result)
|
||||
require.NoError(t, err, "querying for total file count should not produce an error")
|
||||
require.NoError(t, err, "querying for total file count should not produce an error")
|
||||
|
||||
require.ElementsMatch(t, fullPathHours[i], result.Paths, "paths should match expected value")
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, fullPathHours[i], result.Paths, "paths should match expected value")
|
||||
}
|
||||
|
||||
// clean up the directory
|
||||
if tc.logDir == "/logs" {
|
||||
require.NoError(t, tc.afs.RemoveAll(tc.logDir), "removing directory should not produce an error")
|
||||
// cleanup each importDB
|
||||
for _, db := range tc.importDBs {
|
||||
// clean up the directory if we are using a mock directory
|
||||
// if tc.logDir == "/logs" {
|
||||
if strings.HasPrefix(db.logDir, "/logs") {
|
||||
require.NoError(t, tc.afs.RemoveAll(db.logDir), "removing directory should not produce an error")
|
||||
}
|
||||
|
||||
// clean up the database
|
||||
err := c.server.DeleteSensorDB(db.name)
|
||||
require.NoError(t, err, "dropping database should not produce an error")
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -230,7 +390,7 @@ func createMockZeekLogs(t *testing.T, afs afero.Fs, directory string, files []st
|
||||
"1715641234.367201\tCxT125\t10.0.0.5\t52.12.0.5\n",
|
||||
)
|
||||
}
|
||||
err = afero.WriteFile(afs, filepath.Join(directory, file), data, os.FileMode(0o775))
|
||||
err := afero.WriteFile(afs, filepath.Join(directory, file), data, os.FileMode(0o775))
|
||||
require.NoError(t, err, "creating files should not produce an error")
|
||||
}
|
||||
}
|
||||
@@ -949,21 +1109,25 @@ func TestValidateDatabaseName(t *testing.T) {
|
||||
}
|
||||
|
||||
tests := []testCase{
|
||||
{name: "Common name, dnscat2_ja3_strobe", db: "dnscat2_ja3_strobe"},
|
||||
{name: "Common name, combined__0000_rolling", db: "combined__0000_rolling"},
|
||||
{name: "Common name, seconion_2024_05_15", db: "combined__0000_rolling"},
|
||||
{name: "All alpha characters", db: "vsagent"},
|
||||
{name: "All alphanumeric characters", db: "dnscat20"},
|
||||
{name: "All numeric characters", db: "2024", shouldErr: true},
|
||||
{name: "Starting with a number", db: "2vsagent", shouldErr: true},
|
||||
{name: "Starting with a capital letter", db: "Vsagent", shouldErr: true},
|
||||
{name: "All caps", db: "INFORMATION_SCHEMA", shouldErr: true},
|
||||
{name: "Contains special characters", db: "ch!ck3n$tr!p", shouldErr: true},
|
||||
{name: "Contains a hyphen", db: "combined__0000-rolling", shouldErr: true},
|
||||
{name: "Starting with an underscore", db: "_vsagent", shouldErr: true},
|
||||
{name: "Ending with underscore", db: "dnscat2_", shouldErr: true},
|
||||
{name: "Length >63 characters", db: "i_am_a_very_long_database_name_that_is_over_63_characters_long_and_should_fail", shouldErr: true},
|
||||
{name: "Name is reserved: default", db: "default", shouldErr: true},
|
||||
{name: "Name is reserved: system", db: "system", shouldErr: true},
|
||||
{name: "Name is reserved: information_schema", db: "information_schema", shouldErr: true},
|
||||
{name: "Name is reserved: metadatabase", db: "metadatabase", shouldErr: true},
|
||||
{name: "Contains special characters", db: "ch!ck3n$tr!p", shouldErr: true},
|
||||
{name: "Contains a hyphen", db: "combined__0000-rolling", shouldErr: true},
|
||||
{name: "Ends with underscore", db: "dnscat2_", shouldErr: true},
|
||||
{name: "Common name, dnscat2_ja3_strobe", db: "dnscat2_ja3_strobe"},
|
||||
{name: "Common name, combined__0000_rolling", db: "combined__0000_rolling"},
|
||||
{name: "Common name, seconion_2024_05_15", db: "combined__0000_rolling"},
|
||||
{name: "Empty string", db: "", shouldErr: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -973,3 +1137,114 @@ func TestValidateDatabaseName(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateLogDirectory(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
logDir string
|
||||
setup func(afs afero.Fs)
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Directory",
|
||||
logDir: "/validlogdir",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afs.Mkdir("/validlogdir", 0755))
|
||||
require.NoError(t, afero.WriteFile(afs, "/validlogdir/file.txt", []byte("content"), 0644))
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Directory",
|
||||
logDir: "/emptylogdir",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afs.Mkdir("/emptylogdir", 0755))
|
||||
},
|
||||
expectedError: util.ErrDirIsEmpty,
|
||||
},
|
||||
{
|
||||
name: "Path is a File",
|
||||
logDir: "/logfile.txt",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afero.WriteFile(afs, "/logfile.txt", []byte("content"), 0644))
|
||||
},
|
||||
expectedError: util.ErrPathIsNotDir,
|
||||
},
|
||||
{
|
||||
name: "Empty Log Directory",
|
||||
logDir: "",
|
||||
setup: func(_ afero.Fs) {},
|
||||
expectedError: cmd.ErrMissingLogDirectory,
|
||||
},
|
||||
{
|
||||
name: "Invalid Relative Path",
|
||||
logDir: "~/invalid/dir",
|
||||
setup: func(_ afero.Fs) {},
|
||||
expectedError: util.ErrDirDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "Non-Existent Directory",
|
||||
logDir: "/nonexistentdir",
|
||||
setup: func(_ afero.Fs) {},
|
||||
expectedError: util.ErrDirDoesNotExist,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
afs := afero.NewMemMapFs()
|
||||
test.setup(afs)
|
||||
|
||||
err := cmd.ValidateLogDirectory(afs, test.logDir)
|
||||
|
||||
if test.expectedError != nil {
|
||||
require.Error(t, err, "error should not be nil")
|
||||
require.ErrorContains(t, err, test.expectedError.Error(), "error message should contain expected value")
|
||||
} else {
|
||||
require.NoError(t, err, "validating log directory should not produce an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFolderDate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
folder string
|
||||
expectedTime time.Time
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Date Folder",
|
||||
folder: "2023-06-01",
|
||||
expectedTime: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Invalid Date Folder",
|
||||
folder: "invalid-folder",
|
||||
expectedTime: time.Date(2006, 1, 2, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "Empty Folder Name",
|
||||
folder: "",
|
||||
expectedTime: time.Unix(0, 0),
|
||||
expectedError: fmt.Errorf("folder name cannot be empty"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := cmd.ParseFolderDate(test.folder)
|
||||
|
||||
if test.expectedError != nil {
|
||||
require.Error(t, err, "error should not be nil")
|
||||
require.ErrorContains(t, err, test.expectedError.Error(), "error message should contain expected value")
|
||||
|
||||
} else {
|
||||
require.NoError(t, err, "parsing folder date should not produce an error")
|
||||
}
|
||||
|
||||
require.Equal(t, test.expectedTime, result, "the result should match the expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -34,13 +34,19 @@ var ListCommand = &cli.Command{
|
||||
// set up file system interface
|
||||
afs := afero.NewOsFs()
|
||||
|
||||
// load config file
|
||||
cfg, err := config.ReadFileConfig(afs, cCtx.String("config"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the delete command
|
||||
if err := runListCmd(afs, cCtx.String("config")); err != nil {
|
||||
if err := runListCmd(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check for updates after running the command
|
||||
if err := CheckForUpdate(cCtx, afero.NewOsFs()); err != nil {
|
||||
if err := CheckForUpdate(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -48,12 +54,7 @@ var ListCommand = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func runListCmd(afs afero.Fs, configPath string) error {
|
||||
|
||||
cfg, err := config.ReadFileConfig(afs, configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func runListCmd(cfg *config.Config) error {
|
||||
|
||||
// connect to server
|
||||
server, err := database.ConnectToServer(context.Background(), cfg)
|
||||
|
||||
@@ -66,4 +66,10 @@ func (c *CmdTestSuite) TestFormatListTable() {
|
||||
require.Equal(expectedDBs[i].rolling, strings.TrimSpace(cols[1]))
|
||||
require.Equal(expectedDBs[i].tsRange, strings.TrimSpace(cols[2]))
|
||||
}
|
||||
|
||||
// clean up
|
||||
for _, db := range expectedDBs {
|
||||
err := c.server.DeleteSensorDB(db.name)
|
||||
require.NoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -35,13 +35,14 @@ var ValidateConfigCommand = &cli.Command{
|
||||
afs := afero.NewOsFs()
|
||||
|
||||
// validate config file
|
||||
if err := RunValidateConfigCommand(afs, cCtx.String("config")); err != nil {
|
||||
cfg, err := RunValidateConfigCommand(afs, cCtx.String("config"))
|
||||
if err != nil {
|
||||
fmt.Printf("\n\t[!] Configuration file is not valid...")
|
||||
return err
|
||||
}
|
||||
|
||||
// check for updates after running the command
|
||||
if err := CheckForUpdate(cCtx, afero.NewOsFs()); err != nil {
|
||||
if err := CheckForUpdate(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -49,21 +50,21 @@ var ValidateConfigCommand = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func RunValidateConfigCommand(afs afero.Fs, configPath string) error {
|
||||
func RunValidateConfigCommand(afs afero.Fs, configPath string) (*config.Config, error) {
|
||||
// validate config file path
|
||||
if err := ValidateConfigPath(afs, configPath); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// load config path
|
||||
_, err := config.ReadFileConfig(afs, configPath)
|
||||
cfg, err := config.ReadFileConfig(afs, configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("\n\t[✨] Configuration file is valid \n\n")
|
||||
|
||||
return nil
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func ValidateConfigPath(afs afero.Fs, configPath string) error {
|
||||
|
||||
+5
-3
@@ -46,8 +46,6 @@ var ViewCommand = &cli.Command{
|
||||
ConfigFlag(false),
|
||||
},
|
||||
Action: func(cCtx *cli.Context) error {
|
||||
afs := afero.NewOsFs()
|
||||
|
||||
// flags must go before the argument, otherwise they won't be applied ._.
|
||||
// we can either make the db name a flag or see if cobra is any better
|
||||
if !cCtx.Args().Present() {
|
||||
@@ -79,18 +77,22 @@ var ViewCommand = &cli.Command{
|
||||
}
|
||||
}
|
||||
|
||||
// set up file system interface
|
||||
afs := afero.NewOsFs()
|
||||
|
||||
// load config file
|
||||
cfg, err := config.ReadFileConfig(afs, cCtx.String("config"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// run the view command
|
||||
if err := runViewCmd(cfg, cCtx.Args().First(), cCtx.Bool("stdout"), cCtx.String("search"), cCtx.Int("limit")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check for updates after running the command
|
||||
if err := CheckForUpdate(cCtx, afero.NewOsFs()); err != nil {
|
||||
if err := CheckForUpdate(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ var ErrNoMetaDBImportRecordForDatabase = errors.New("no import record found for
|
||||
var ErrDatabaseNotFound = errors.New("database does not exist")
|
||||
var ErrDatabaseNameEmpty = errors.New("database name cannot be empty")
|
||||
var ErrMissingConfig = errors.New("config cannot be nil")
|
||||
var errImportTwiceNonRolling = errors.New("cannot import more than once to a non-rolling database")
|
||||
var ErrImportTwiceNonRolling = errors.New("cannot import more than once to a non-rolling database")
|
||||
var errRollingStatusFailure = errors.New("failed to detect rolling status of given import database")
|
||||
var errRollingFlagMissing = errors.New("cannot import non-rolling data to a rolling database")
|
||||
|
||||
@@ -337,8 +337,8 @@ func (server *ServerConn) checkRolling(dbName string, rollingFlag bool, rebuildF
|
||||
// command is requesting to import data as rolling, but dataset is not rolling
|
||||
case rollingFlag && !rolling && !rebuildFlag:
|
||||
logger.Warn().Str("database", dbName).
|
||||
Msg(errImportTwiceNonRolling.Error())
|
||||
return rolling, errImportTwiceNonRolling
|
||||
Msg(ErrImportTwiceNonRolling.Error())
|
||||
return rolling, ErrImportTwiceNonRolling
|
||||
|
||||
// command is requesting to import data as non-rolling, but dataset is rolling
|
||||
case rolling && !rollingFlag && !rebuildFlag:
|
||||
|
||||
+38
-37
@@ -66,6 +66,7 @@ func init() {
|
||||
privateIPBlocks = privateIPs
|
||||
}
|
||||
|
||||
// NewFixedStringHash creates a FixedString from a hash of all the passed in strings
|
||||
func NewFixedStringHash(args ...string) (FixedString, error) {
|
||||
if len(args) == 0 {
|
||||
return FixedString{}, errors.New("no arguments provided")
|
||||
@@ -85,26 +86,23 @@ func NewFixedStringHash(args ...string) (FixedString, error) {
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// NewFixedStringFromString creates a FixedString from a passed in hex string
|
||||
func NewFixedStringFromHex(h string) (FixedString, error) {
|
||||
if h == "" {
|
||||
return FixedString{}, errors.New("hex string is empty")
|
||||
}
|
||||
|
||||
data, err := hex.DecodeString(h)
|
||||
if err != nil {
|
||||
return FixedString{}, err
|
||||
return FixedString{}, fmt.Errorf("error decoding hex string: %w", err)
|
||||
}
|
||||
var fixed [16]byte
|
||||
copy(fixed[:], data[:16])
|
||||
copy(fixed[:], data)
|
||||
return FixedString{
|
||||
Data: fixed,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ValidFQDN(value string) bool {
|
||||
// Regular expression for validating FQDN
|
||||
// This pattern requires at least two labels (separated by dots), with each label starting and ending with an alphanumeric character.
|
||||
// Labels in between can have hyphens. The last label (TLD) must be at least two characters long, with only letters.
|
||||
re := regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
|
||||
return re.MatchString(value)
|
||||
}
|
||||
|
||||
func (bin *FixedString) Hex() string {
|
||||
return strings.ToUpper(hex.EncodeToString(bin.Data[:]))
|
||||
}
|
||||
@@ -127,6 +125,14 @@ func (bin FixedString) Value() (driver.Value, error) {
|
||||
return &bin.val, nil
|
||||
}
|
||||
|
||||
func ValidFQDN(value string) bool {
|
||||
// Regular expression for validating FQDN
|
||||
// This pattern requires at least two labels (separated by dots), with each label starting and ending with an alphanumeric character.
|
||||
// Labels in between can have hyphens. The last label (TLD) must be at least two characters long, with only letters.
|
||||
re := regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$`)
|
||||
return re.MatchString(value)
|
||||
}
|
||||
|
||||
// ContainsIP checks if a collection of subnets contains an IP
|
||||
func ContainsIP(subnets []*net.IPNet, ip net.IP) bool {
|
||||
// cache IPv4 conversion so it not performed every in every Contains call
|
||||
@@ -294,23 +300,7 @@ func GetRelativeFirstSeenTimestamp(useCurrentTime bool, maxTimestamp time.Time)
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// func ParseRelativePath(dir string) (string, error) {
|
||||
// // if path is home, parse and set home dir
|
||||
// if dir[:2] == "~/" {
|
||||
// home, err := os.UserHomeDir()
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// return filepath.Join(home, dir[2:]), nil
|
||||
// }
|
||||
// // otherwise, get the path relative to the current working directory
|
||||
// currentDir, err := os.Getwd()
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
// return filepath.Join(currentDir, dir), nil
|
||||
// }
|
||||
|
||||
// ParseRelativePath parses a given directory path and returns the absolute path
|
||||
func ParseRelativePath(dir string) (string, error) {
|
||||
// validate parameters
|
||||
if dir == "" {
|
||||
@@ -337,7 +327,6 @@ func ParseRelativePath(dir string) (string, error) {
|
||||
return dir, nil
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ValidateDirectory returns whether a directory exists and is empty
|
||||
@@ -392,6 +381,7 @@ func ValidateFile(afs afero.Fs, file string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePath validates a given path
|
||||
func validatePath(afs afero.Fs, path string) (bool, bool, bool, error) {
|
||||
var exists, isDir, isEmpty bool
|
||||
|
||||
@@ -428,31 +418,42 @@ func validatePath(afs afero.Fs, path string) (bool, bool, bool, error) {
|
||||
|
||||
// CheckForNewerVersion checks if a newer version of the project is available on the GitHub repository
|
||||
func CheckForNewerVersion(client *github.Client, currentVersion string) (bool, string, error) {
|
||||
|
||||
// Get the latest release
|
||||
latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "activecm", "rita")
|
||||
// get the latest version
|
||||
latestVersion, err := GetLatestReleaseVersion(client, "activecm", "rita")
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("error fetching latest release: %w", err)
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
// Get the latest version from release tag name
|
||||
latestVersion := latestRelease.GetTagName()
|
||||
|
||||
// Parse the current and latest versions
|
||||
// parse the current version
|
||||
currentSemver, err := semver.ParseTolerant(currentVersion)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("error parsing current version: %w", err)
|
||||
}
|
||||
|
||||
// parse the latest version
|
||||
latestSemver, err := semver.ParseTolerant(latestVersion)
|
||||
if err != nil {
|
||||
return false, "", fmt.Errorf("error parsing latest version: %w", err)
|
||||
}
|
||||
|
||||
// Compare the versions
|
||||
// compare the versions
|
||||
if latestSemver.GT(currentSemver) {
|
||||
return true, latestVersion, nil
|
||||
}
|
||||
|
||||
return false, latestVersion, nil
|
||||
}
|
||||
|
||||
// GetLatestReleaseVersion gets the latest release version from the GitHub repository
|
||||
func GetLatestReleaseVersion(client *github.Client, owner, repo string) (string, error) {
|
||||
// get the latest release
|
||||
latestRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), owner, repo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error fetching latest release: %w", err)
|
||||
}
|
||||
|
||||
// get the latest version from release tag name
|
||||
latestVersion := latestRelease.GetTagName()
|
||||
|
||||
return latestVersion, nil
|
||||
}
|
||||
|
||||
+704
-56
@@ -2,6 +2,7 @@ package util
|
||||
|
||||
import (
|
||||
"crypto/md5" // #nosec G501
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -95,6 +97,240 @@ func TestNewFixedStringHash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFixedStringFromHex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected FixedString
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Hex String",
|
||||
input: "00112233445566778899aabbccddeeff",
|
||||
expected: FixedString{
|
||||
Data: [16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Valid Hex String Shorter than 16 bytes",
|
||||
input: "0011223344556677",
|
||||
expected: FixedString{
|
||||
Data: [16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Valid Hex String Longer than 16 bytes",
|
||||
input: "00112233445566778899aabbccddeeffaabbccddeeff",
|
||||
expected: FixedString{
|
||||
Data: [16]byte{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Hex String",
|
||||
input: "invalidhexstring",
|
||||
expected: FixedString{},
|
||||
expectedError: fmt.Errorf("error decoding hex string: "),
|
||||
},
|
||||
{
|
||||
name: "Empty Hex String",
|
||||
input: "",
|
||||
expected: FixedString{},
|
||||
expectedError: fmt.Errorf("hex string is empty"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := NewFixedStringFromHex(test.input)
|
||||
|
||||
if test.expectedError != nil {
|
||||
require.Error(t, err, "error should not be nil")
|
||||
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
|
||||
} else {
|
||||
require.NoError(t, err, "converting hex to fixed string should not produce an error")
|
||||
require.Equal(t, test.expected, result, "the result should match the expected value")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixedString_Hex(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input FixedString
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "All Zeros",
|
||||
input: FixedString{Data: [16]byte{}},
|
||||
expected: "00000000000000000000000000000000",
|
||||
},
|
||||
{
|
||||
name: "Mixed Data",
|
||||
input: FixedString{Data: [16]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}},
|
||||
expected: "000102030405060708090A0B0C0D0E0F",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := test.input.Hex()
|
||||
require.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixedString_MarshalBinary(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input FixedString
|
||||
expected []byte
|
||||
}{
|
||||
{
|
||||
name: "All Zeros",
|
||||
input: FixedString{Data: [16]byte{}},
|
||||
expected: make([]byte, 16),
|
||||
},
|
||||
{
|
||||
name: "Mixed Data",
|
||||
input: FixedString{Data: [16]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}},
|
||||
expected: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := test.input.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixedString_UnmarshalBinary(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected FixedString
|
||||
}{
|
||||
{
|
||||
name: "All Zeros",
|
||||
input: make([]byte, 16),
|
||||
expected: FixedString{Data: [16]byte{}},
|
||||
},
|
||||
{
|
||||
name: "Mixed Data",
|
||||
input: []byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF},
|
||||
expected: FixedString{Data: [16]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var result FixedString
|
||||
err := result.UnmarshalBinary(test.input)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixedString_Value(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input FixedString
|
||||
expected driver.Value
|
||||
}{
|
||||
{
|
||||
name: "Default Value",
|
||||
input: FixedString{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "With Value",
|
||||
input: FixedString{val: "example"},
|
||||
expected: "example",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := test.input.Value()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, test.expected, *result.(*string))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidFQDN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Valid FQDN",
|
||||
value: "example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Valid FQDN with Multiple Subdomains",
|
||||
value: "sub.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Valid FQDN with Hyphen",
|
||||
value: "sub-domain.example.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Single Label",
|
||||
value: "example",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Trailing Dot",
|
||||
value: "example.com.",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid Underscore",
|
||||
value: "sub_domain.example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid Spaces",
|
||||
value: "example .com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid Special Characters",
|
||||
value: "exa$mple.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "TLD Too Short",
|
||||
value: "example.c",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty String",
|
||||
value: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := ValidFQDN(test.value)
|
||||
require.Equal(t, test.expected, result, "the result should match the expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -433,54 +669,6 @@ func TestParseNetworkID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTimestamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
timestamp time.Time
|
||||
expectedTime time.Time
|
||||
replaced bool
|
||||
}{
|
||||
{
|
||||
name: "Valid timestamp",
|
||||
timestamp: time.Date(2024, time.June, 3, 23, 24, 10, 0, time.Local),
|
||||
expectedTime: time.Date(2024, time.June, 3, 23, 24, 10, 0, time.Local),
|
||||
replaced: false,
|
||||
},
|
||||
{
|
||||
name: "Log Floating-Pont Timestamp",
|
||||
timestamp: time.Unix(1517336108, int64((0.231879)*1e9)), // 1517336108.231879
|
||||
expectedTime: time.Unix(1517336108, 231879000),
|
||||
replaced: false,
|
||||
},
|
||||
{
|
||||
name: "Unset Timestamp",
|
||||
timestamp: time.Time{},
|
||||
expectedTime: time.Unix(0, 1),
|
||||
replaced: true,
|
||||
},
|
||||
{
|
||||
name: "MaxInt64 timestamp",
|
||||
timestamp: time.Unix(math.MaxInt64, 0),
|
||||
expectedTime: time.Unix(0, 1),
|
||||
replaced: true,
|
||||
},
|
||||
{
|
||||
name: "Negative timestamp",
|
||||
timestamp: time.Unix(-1, 0),
|
||||
expectedTime: time.Unix(0, 1),
|
||||
replaced: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ts, replaced := ValidateTimestamp(test.timestamp)
|
||||
require.Equal(t, test.expectedTime, ts, "timestamp should match expected value")
|
||||
require.Equal(t, test.replaced, replaced, "replaced should match expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -564,9 +752,198 @@ func TestContainsDomain(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureSliceContainsAll(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []string
|
||||
mandatory []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "All elements present",
|
||||
data: []string{"a", "b", "c"},
|
||||
mandatory: []string{"a", "b"},
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "Some elements missing",
|
||||
data: []string{"a", "b"},
|
||||
mandatory: []string{"a", "b", "c"},
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "No elements present",
|
||||
data: []string{},
|
||||
mandatory: []string{"a", "b", "c"},
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "Empty mandatory list",
|
||||
data: []string{"a", "b", "c"},
|
||||
mandatory: []string{},
|
||||
expected: []string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
name: "No elements in both lists",
|
||||
data: []string{},
|
||||
mandatory: []string{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "Duplicate elements in mandatory list",
|
||||
data: []string{"a", "b"},
|
||||
mandatory: []string{"b", "c", "c"},
|
||||
expected: []string{"a", "b", "c", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := EnsureSliceContainsAll(test.data, test.mandatory)
|
||||
require.ElementsMatch(t, test.expected, result, "resulting list should match expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortUInt32s(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []uint32
|
||||
expected []uint32
|
||||
}{
|
||||
{
|
||||
name: "Already sorted",
|
||||
data: []uint32{1, 2, 3, 4, 5},
|
||||
expected: []uint32{1, 2, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
name: "Reverse order",
|
||||
data: []uint32{5, 4, 3, 2, 1},
|
||||
expected: []uint32{1, 2, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
name: "Unsorted",
|
||||
data: []uint32{3, 1, 4, 5, 2},
|
||||
expected: []uint32{1, 2, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
name: "With duplicates",
|
||||
data: []uint32{3, 1, 4, 1, 5, 2, 3},
|
||||
expected: []uint32{1, 1, 2, 3, 3, 4, 5},
|
||||
},
|
||||
{
|
||||
name: "Single element",
|
||||
data: []uint32{1},
|
||||
expected: []uint32{1},
|
||||
},
|
||||
{
|
||||
name: "Empty slice",
|
||||
data: []uint32{},
|
||||
expected: []uint32{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
SortUInt32s(test.data)
|
||||
require.Equal(t, test.expected, test.data, "the sorted data should match the expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUInt32sAreSorted(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []uint32
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Sorted data",
|
||||
data: []uint32{1, 2, 3, 4, 5},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Unsorted data",
|
||||
data: []uint32{5, 3, 4, 1, 2},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Empty data",
|
||||
data: []uint32{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Single element",
|
||||
data: []uint32{42},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "All elements equal",
|
||||
data: []uint32{7, 7, 7, 7},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := UInt32sAreSorted(test.data)
|
||||
require.Equal(t, test.expected, result, "the result should match the expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateTimestamp(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
timestamp time.Time
|
||||
expectedTime time.Time
|
||||
replaced bool
|
||||
}{
|
||||
{
|
||||
name: "Valid timestamp",
|
||||
timestamp: time.Date(2024, time.June, 3, 23, 24, 10, 0, time.Local),
|
||||
expectedTime: time.Date(2024, time.June, 3, 23, 24, 10, 0, time.Local),
|
||||
replaced: false,
|
||||
},
|
||||
{
|
||||
name: "Log Floating-Pont Timestamp",
|
||||
timestamp: time.Unix(1517336108, int64((0.231879)*1e9)), // 1517336108.231879
|
||||
expectedTime: time.Unix(1517336108, 231879000),
|
||||
replaced: false,
|
||||
},
|
||||
{
|
||||
name: "Unset Timestamp",
|
||||
timestamp: time.Time{},
|
||||
expectedTime: time.Unix(0, 1),
|
||||
replaced: true,
|
||||
},
|
||||
{
|
||||
name: "MaxInt64 timestamp",
|
||||
timestamp: time.Unix(math.MaxInt64, 0),
|
||||
expectedTime: time.Unix(0, 1),
|
||||
replaced: true,
|
||||
},
|
||||
{
|
||||
name: "Negative timestamp",
|
||||
timestamp: time.Unix(-1, 0),
|
||||
expectedTime: time.Unix(0, 1),
|
||||
replaced: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ts, replaced := ValidateTimestamp(test.timestamp)
|
||||
require.Equal(t, test.expectedTime, ts, "timestamp should match expected value")
|
||||
require.Equal(t, test.replaced, replaced, "replaced should match expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRelativePath(t *testing.T) {
|
||||
home, err := os.UserHomeDir()
|
||||
require.NoError(t, err)
|
||||
fmt.Println("home: ", home)
|
||||
|
||||
workingDir, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
@@ -577,39 +954,51 @@ func TestParseRelativePath(t *testing.T) {
|
||||
name string
|
||||
path string
|
||||
expected string
|
||||
expectErr bool
|
||||
expectErr error
|
||||
}{
|
||||
{
|
||||
name: "Home directory",
|
||||
path: "~/data",
|
||||
expected: home + "/data",
|
||||
expectErr: false,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Current directory path",
|
||||
path: "./",
|
||||
expected: workingDir,
|
||||
// expectedPath: filepath.Join(currentDir, "./mydir"),
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Relative directory - 1 deep",
|
||||
path: "./data",
|
||||
expected: workingDir + "/data",
|
||||
expectErr: false,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Relative directory - 2 deep",
|
||||
path: "../data",
|
||||
expected: currentDir + "/data",
|
||||
expectErr: false,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Absolute path",
|
||||
path: "/home/logs",
|
||||
expected: "/home/logs",
|
||||
expectErr: false,
|
||||
expectErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty path",
|
||||
expected: "",
|
||||
expectErr: ErrInvalidPath,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := ParseRelativePath(test.path)
|
||||
if test.expectErr {
|
||||
require.Error(t, err, "error was expected")
|
||||
if test.expectErr != nil {
|
||||
require.EqualError(t, err, test.expectErr.Error(), "error should match expected value")
|
||||
} else {
|
||||
require.NoError(t, err, "parsing relative path should not produce an error")
|
||||
require.Equal(t, test.expected, result, "relative path should match expected value, got: %s, expected: %s", result, test.expected)
|
||||
@@ -618,7 +1007,208 @@ func TestParseRelativePath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckForNewerVersion tests the CheckForNewerVersion function
|
||||
func TestValidateDirectory(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(afs afero.Fs)
|
||||
dir string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Directory is Valid",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afs.Mkdir("/nonemptydir", 0755))
|
||||
require.NoError(t, afero.WriteFile(afs, "/nonemptydir/file.txt", []byte("content"), 0644))
|
||||
},
|
||||
dir: "/nonemptydir",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Directory Does Not Exist",
|
||||
setup: func(_ afero.Fs) {},
|
||||
dir: "/nonexistent",
|
||||
expectedError: ErrDirDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "Path is Not a Directory",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte("content"), 0644))
|
||||
},
|
||||
dir: "/file.txt",
|
||||
expectedError: ErrPathIsNotDir,
|
||||
},
|
||||
{
|
||||
name: "Directory is Empty",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afs.Mkdir("/emptydir", 0755))
|
||||
},
|
||||
dir: "/emptydir",
|
||||
expectedError: ErrDirIsEmpty,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
afs := afero.NewMemMapFs()
|
||||
test.setup(afs)
|
||||
|
||||
err := ValidateDirectory(afs, test.dir)
|
||||
if test.expectedError != nil {
|
||||
require.Error(t, err, "error should not be nil")
|
||||
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
|
||||
} else {
|
||||
require.NoError(t, err, "validating directory should not produce an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(afs afero.Fs)
|
||||
file string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "File is Valid",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte("content"), 0644))
|
||||
},
|
||||
file: "/file.txt",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "File is Empty",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afero.WriteFile(afs, "/emptyfile.txt", []byte(""), 0644))
|
||||
},
|
||||
file: "/emptyfile.txt",
|
||||
expectedError: ErrFileIsEmtpy,
|
||||
},
|
||||
{
|
||||
name: "File Does Not Exist",
|
||||
setup: func(_ afero.Fs) {},
|
||||
file: "/nonexistent",
|
||||
expectedError: ErrFileDoesNotExist,
|
||||
},
|
||||
{
|
||||
name: "Path is a Directory",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afs.Mkdir("/directory", 0755))
|
||||
},
|
||||
file: "/directory",
|
||||
expectedError: ErrPathIsDir,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
afs := afero.NewMemMapFs()
|
||||
test.setup(afs)
|
||||
|
||||
err := ValidateFile(afs, test.file)
|
||||
if test.expectedError != nil {
|
||||
require.Error(t, err, "error should not be nil")
|
||||
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
|
||||
} else {
|
||||
require.NoError(t, err, "validating file should not produce an error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(afs afero.Fs)
|
||||
path string
|
||||
expected [3]bool // exists, isDir, isEmpty
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Path is Valid Non-Empty File",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte("content"), 0644))
|
||||
},
|
||||
path: "/file.txt",
|
||||
expected: [3]bool{true, false, false},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Path is Valid Empty File",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afero.WriteFile(afs, "/file.txt", []byte(""), 0644))
|
||||
},
|
||||
path: "/file.txt",
|
||||
expected: [3]bool{true, false, true},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Path is Valid Non-Empty Directory",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afs.Mkdir("/nonemptydir", 0755))
|
||||
require.NoError(t, afero.WriteFile(afs, "/nonemptydir/file.txt", []byte("content"), 0644))
|
||||
},
|
||||
path: "/nonemptydir",
|
||||
expected: [3]bool{true, true, false},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Path is Valid Empty Directory",
|
||||
setup: func(afs afero.Fs) {
|
||||
require.NoError(t, afs.Mkdir("/emptydir", 0755))
|
||||
},
|
||||
path: "/emptydir",
|
||||
expected: [3]bool{true, true, true},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Non-Existent Path",
|
||||
setup: func(_ afero.Fs) {},
|
||||
path: "/nonexistent",
|
||||
expected: [3]bool{false, false, false},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Path",
|
||||
setup: func(_ afero.Fs) {},
|
||||
path: "",
|
||||
expected: [3]bool{false, false, false},
|
||||
expectedError: ErrInvalidPath,
|
||||
},
|
||||
{
|
||||
name: "Nil filesystem",
|
||||
setup: func(_ afero.Fs) {},
|
||||
path: "/some/path",
|
||||
expected: [3]bool{false, false, false},
|
||||
expectedError: fmt.Errorf("filesystem is nil"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var afs afero.Fs
|
||||
if test.name != "Nil filesystem" {
|
||||
afs = afero.NewMemMapFs()
|
||||
}
|
||||
test.setup(afs)
|
||||
|
||||
exists, isDir, isEmpty, err := validatePath(afs, test.path)
|
||||
|
||||
if test.expectedError != nil {
|
||||
// require.Error(t, err)
|
||||
require.ErrorContains(t, err, test.expectedError.Error(), "error should contain expected value")
|
||||
} else {
|
||||
require.NoError(t, err, "validating path should not produce an error")
|
||||
require.Equal(t, test.expected[0], exists, "exist flag should be %v", test.expected[0])
|
||||
require.Equal(t, test.expected[1], isDir, "isDir flag should be %v", test.expected[1])
|
||||
require.Equal(t, test.expected[2], isEmpty, "isEmpty flag should be %v", test.expected[2])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForNewerVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -687,3 +1277,61 @@ func TestCheckForNewerVersion(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLatestReleaseVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
owner string
|
||||
repo string
|
||||
latestVersion string
|
||||
expected string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Latest Release",
|
||||
owner: "activecm",
|
||||
repo: "rita",
|
||||
latestVersion: "v2.0.0",
|
||||
expected: "v2.0.0",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "Error Fetching Latest Release",
|
||||
owner: "activecm",
|
||||
repo: "rita",
|
||||
expected: "",
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Create a test server
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
if test.expectedError {
|
||||
http.Error(w, "error", http.StatusInternalServerError)
|
||||
} else {
|
||||
fmt.Fprintf(w, `{"tag_name": "%s"}`, test.latestVersion)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Override the GitHub client base URL
|
||||
client := github.NewClient(nil)
|
||||
newBaseURL, err := client.BaseURL.Parse(ts.URL + "/")
|
||||
require.NoError(t, err, "failed to parse base URL")
|
||||
client.BaseURL = newBaseURL
|
||||
|
||||
result, err := GetLatestReleaseVersion(client, test.owner, test.repo)
|
||||
|
||||
if test.expectedError {
|
||||
require.Error(t, err, "error should not be nil")
|
||||
require.ErrorContains(t, err, "error fetching latest release", "error should contain expected value")
|
||||
} else {
|
||||
require.NoError(t, err, "fetching latest release should not produce an error")
|
||||
require.Equal(t, test.expected, result, "the result should match the expected value")
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+33
-16
@@ -42,25 +42,36 @@ func (m *sidebarModel) Init() tea.Cmd {
|
||||
m.Viewport.SetContent(m.getSidebarContents())
|
||||
return nil
|
||||
}
|
||||
|
||||
type UpdateItem *Item
|
||||
|
||||
func (m *sidebarModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
// if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
|
||||
// return m, tea.Quit
|
||||
// }
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case *Item:
|
||||
|
||||
m.Data = msg
|
||||
content := m.getSidebarContents()
|
||||
numlines := strings.Count(content, "\n") + 1 + 2
|
||||
|
||||
numToClear := m.Viewport.Height - numlines
|
||||
if numToClear > 0 {
|
||||
spaces := m.Viewport.Width - 2
|
||||
for i := 0; i < numToClear; i++ {
|
||||
content += fmt.Sprintf("%*s\n", spaces, "")
|
||||
}
|
||||
}
|
||||
|
||||
m.Viewport.SetContent(content)
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
cmds = append(cmds, viewport.Sync(m.Viewport))
|
||||
|
||||
// return m, nil
|
||||
}
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *sidebarModel) View() string {
|
||||
m.Viewport.SetContent(m.getSidebarContents())
|
||||
m.Viewport.Height = m.Height
|
||||
borderColor := mauve
|
||||
if m.ScrollEnabled {
|
||||
borderColor = green
|
||||
@@ -71,13 +82,17 @@ func (m *sidebarModel) View() string {
|
||||
Padding(0, 1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(borderColor)
|
||||
hi := style.Render(m.Viewport.View())
|
||||
return lipgloss.NewStyle().Render(hi)
|
||||
// Border(lipgloss.NormalBorder())
|
||||
sidebar := style.Render(m.Viewport.View())
|
||||
return lipgloss.NewStyle().Render(sidebar)
|
||||
|
||||
}
|
||||
|
||||
// getSidebarContents gets and formats the data to be displayed in the sidebar
|
||||
func (m *sidebarModel) getSidebarContents() string {
|
||||
if m.Data == nil {
|
||||
return lipgloss.NewStyle().Foreground(overlay0).Render("No result found.")
|
||||
}
|
||||
|
||||
// get header
|
||||
var target string
|
||||
headerPadding := 2
|
||||
@@ -119,15 +134,17 @@ func (m *sidebarModel) getSidebarContents() string {
|
||||
|
||||
connInfoLabel := sectionStyle.Render("「 Connection Info 」")
|
||||
|
||||
dataStyle := lipgloss.NewStyle().Foreground(defaultTextColor)
|
||||
|
||||
// get connection count
|
||||
connCountStyle := lipgloss.NewStyle().Background(overlay2).Foreground(base).Bold(true).Padding(0, 2)
|
||||
connCountHeader := connCountStyle.Render("Connection Count")
|
||||
connCount := lipgloss.JoinVertical(lipgloss.Top, connCountHeader, fmt.Sprintf("%d", m.Data.Count))
|
||||
connCount := dataStyle.Render(lipgloss.JoinVertical(lipgloss.Top, connCountHeader, fmt.Sprintf("%d", m.Data.Count)))
|
||||
|
||||
// get total bytes
|
||||
bytesHeaderStyle := lipgloss.NewStyle().Background(overlay2).Foreground(base).Bold(true).Padding(0, 2)
|
||||
bytesHeader := bytesHeaderStyle.Render("Total Bytes")
|
||||
bytes := lipgloss.JoinVertical(lipgloss.Top, bytesHeader, m.Data.TotalBytesFormatted)
|
||||
bytes := dataStyle.Render(lipgloss.JoinVertical(lipgloss.Top, bytesHeader, m.Data.TotalBytesFormatted))
|
||||
|
||||
// get port:proto:service
|
||||
portProtoService := m.Data.GetPortProtoService()
|
||||
@@ -145,7 +162,7 @@ func (m *sidebarModel) getSidebarContents() string {
|
||||
|
||||
// render header
|
||||
portsHeader := portsHeaderStyle.Render("Port : Proto : Service")
|
||||
ports = lipgloss.JoinVertical(lipgloss.Top, portsHeader, strings.Join(portProtoService, ","))
|
||||
ports = dataStyle.Render(lipgloss.JoinVertical(lipgloss.Top, portsHeader, strings.Join(portProtoService, "\n")))
|
||||
// strings.Join(portProtoService, "\n")
|
||||
// calculate the number of lines available for port data
|
||||
// remainingLines := m.viewport.Height - (lipgloss.Height(heading) + lipgloss.Height(modifiers) + lipgloss.Height(modifierLabel) + lipgloss.Height(connInfoLabel) + lipgloss.Height(bytes) + lipgloss.Height(connCount))
|
||||
@@ -182,7 +199,7 @@ func (m *sidebarModel) renderModifiers() string {
|
||||
|
||||
width := lipgloss.Width(newMod)
|
||||
if m.Viewport.Width <= width {
|
||||
modifierLines = append(modifierLines, lipgloss.JoinHorizontal(lipgloss.Left, linebreakStyle.Render(currentModifiers)))
|
||||
modifierLines = append(modifierLines, lipgloss.NewStyle().Foreground(defaultTextColor).Render(lipgloss.JoinHorizontal(lipgloss.Left, linebreakStyle.Render(currentModifiers))))
|
||||
currentModifiers = mod
|
||||
if i != len(renderedModifiers)-1 {
|
||||
currentModifiers = newlineStyle.Render(mod)
|
||||
|
||||
+23
-9
@@ -189,7 +189,7 @@ func (m *Model) Init() tea.Cmd {
|
||||
// Update updates the model
|
||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
// make the footer the entire width of the terminal
|
||||
@@ -231,7 +231,8 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// handle filtering
|
||||
case m.SearchBar.TextInput.Focused():
|
||||
cmd = m.handleFiltering(msg)
|
||||
cmd := m.handleFiltering(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// clear filtering (when search bar not focused)
|
||||
case key.Matches(msg, m.keys.clearFilter):
|
||||
@@ -239,18 +240,24 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// handle quiting
|
||||
case key.Matches(msg, m.keys.quit):
|
||||
cmd = tea.Quit
|
||||
cmd := tea.Quit
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// otherwise, handle browsing
|
||||
default:
|
||||
cmd = m.handleBrowsing(msg)
|
||||
cmd := m.handleBrowsing(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
case StillLoadingResults, FooterFlash:
|
||||
_, cmd = m.Footer.Update(msg)
|
||||
_, cmd := m.Footer.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
case FinishedLoadingResults:
|
||||
|
||||
case spinner.TickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.Footer.spinner, cmd = m.Footer.spinner.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
}
|
||||
|
||||
// update sidebar
|
||||
@@ -271,15 +278,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// set sidebar data to the selected item
|
||||
if data, ok := m.List.Rows.Items()[m.List.Rows.Index()].(*Item); ok {
|
||||
m.SideBar.Data = data
|
||||
_, cmd := m.SideBar.Update(data)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
} else {
|
||||
// if there are no items to display, set the sidebar data to an empty item
|
||||
m.SideBar.Data = &Item{}
|
||||
// if there are no items to display, set the sidebar data to nil
|
||||
_, cmd := m.SideBar.Update(nil)
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
return m, cmd
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// View renders the model to the terminal
|
||||
@@ -429,6 +438,11 @@ func (m *Model) requestResults(appendResults bool) {
|
||||
m.Footer.loading = true
|
||||
// time.Sleep(4 * time.Second)
|
||||
|
||||
// reset the server page number if we're not appending results to the rows list
|
||||
if !appendResults {
|
||||
m.serverPage = 0
|
||||
}
|
||||
|
||||
// get results from database
|
||||
items, appliedFilter, err := GetResults(m.db, filter, m.serverPage, m.serverPageSize, m.minTS)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user