From c94db0ac038fd48f2cf611ad6f0cb1b3937e988a Mon Sep 17 00:00:00 2001 From: PS Narayanan Date: Wed, 21 May 2025 09:24:48 +0530 Subject: [PATCH] Document config reloading and fix port filter --- README.md | 30 +++++++++- config.json | 2 +- go.mod | 5 +- pkg/config/config.go | 84 ++++++++++++++++++++++++--- plugins/portfilter/portfilter.go | 34 ++++++++++- plugins/portfilter/portfilter_test.go | 14 +++++ run.sh | 2 +- 7 files changed, 153 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 041ffb9..424a3db 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,20 @@ The behavior of each plugin is driven by configuration, making "ShadowGuard" hig The architecture also facilitates both active and passive modes of operation, allowing the system to either block malicious traffic actively or to monitor and alert on potential threats passively. This flexibility of operation modes allows "ShadowGuard" to be tailored to the specific security posture of your application or API. ## Getting Started: -TODO: Instructions on how to setup "ShadowGuard", its dependencies, and how to get it running. +Install Go 1.20+ and clone this repository. Use the `build.sh` script to prepare the PostgreSQL database and other requirements. + +After the database is running you can start the service using either `go run` or the helper script: + +```shell +go run cmd/main.go +``` + +or + +```shell +chmod +x run.sh +./run.sh +``` ### Database Run `build.sh` to setup install the necessary dependencies including Postgresql and configures the gorm database. @@ -58,12 +71,23 @@ docker build . -t shadow_guard docker run --network=host shadow_guard ``` +## Configuration + +ShadowGuard reads its settings from `config.json` in the project root. You can override the file location with the `SHADOW_CONFIG` environment variable: + +```shell +export SHADOW_CONFIG=/etc/shadowguard.json +./run.sh +``` + +The configuration file is monitored for changes and will be reloaded automatically without restarting the service. + ## Unit Tests: In order to run unit tests, you can use the shell script `run_tests.sh` in the root directory. The unit tests can be ran using convential Go commands. ## Documentation: -TODO: Link to full API documentation, or brief outline of main methods and how to use them. +Refer to the source in the `plugins` directory for examples of middleware and plugin usage. Each plugin implements the `Plugin` interface defined in `pkg/plugin`. Configuration options for each plugin are documented in the corresponding README files when available. ## License: -TODO: Information on the licensing of "ShadowGuard". +The project is currently distributed without a specific license. All rights are reserved by the original authors. diff --git a/config.json b/config.json index f2de38b..9237853 100644 --- a/config.json +++ b/config.json @@ -33,7 +33,7 @@ "publishers": [ { "type": "slack", - "token": "xoxb-198202255696-5682091092327-m8IHyjQEnO6FdIIslpzjq2nz", + "token": "YOUR_SLACK_TOKEN", "channelID": "C5UTW0J6N" } ] diff --git a/go.mod b/go.mod index c53f168..3e14935 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,9 @@ require ( github.com/gorilla/mux v1.8.0 github.com/lib/pq v1.10.9 github.com/oschwald/geoip2-golang v1.9.0 - github.com/slack-go/slack v0.12.2 - gorm.io/driver/postgres v1.5.2 + github.com/slack-go/slack v0.12.2 + github.com/fsnotify/fsnotify v1.6.0 + gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.4 ) diff --git a/pkg/config/config.go b/pkg/config/config.go index 82820fa..56b5543 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,6 +5,10 @@ import ( "io" "log" "os" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" ) // PluginConfig represents the configuration for a single plugin @@ -37,6 +41,69 @@ type Config struct { Endpoints []Endpoint `json:"endpoints"` } +var ( + current *Config + mu sync.RWMutex +) + +func loadConfigFromFile(path string) (*Config, error) { + configJsonFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer configJsonFile.Close() + + byteData, err := io.ReadAll(configJsonFile) + if err != nil { + return nil, err + } + + var cfg Config + if err := json.Unmarshal(byteData, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} + +func watchConfigFile(path string) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Printf("Error creating watcher: %v", err) + return + } + defer watcher.Close() + + dir := filepath.Dir(path) + if err := watcher.Add(dir); err != nil { + log.Printf("Error watching config directory: %v", err) + return + } + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + if event.Op&(fsnotify.Write|fsnotify.Create) != 0 && filepath.Clean(event.Name) == filepath.Clean(path) { + log.Printf("Configuration file changed. Reloading\n") + if cfg, err := loadConfigFromFile(path); err != nil { + log.Printf("Failed to reload configuration: %v", err) + } else { + mu.Lock() + *current = *cfg + mu.Unlock() + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Printf("Watcher error: %v", err) + } + } +} + // Init initializes the configuration from a file. // The config file path can be set dynamically using environment variables. // The default is assumed to be `config.json` in the same directory. @@ -47,18 +114,17 @@ func Init() *Config { } log.Printf("Reading configuration file %s\n", configFilePath) - configJsonFile, err := os.Open(configFilePath) - if err != nil { - panic(err) - } - defer configJsonFile.Close() - byteData, err := io.ReadAll(configJsonFile) + cfg, err := loadConfigFromFile(configFilePath) if err != nil { panic(err) } - var config Config - json.Unmarshal(byteData, &config) + mu.Lock() + current = cfg + mu.Unlock() + + go watchConfigFile(configFilePath) + log.Printf("Configuration file loaded.\n") - return &config + return current } diff --git a/plugins/portfilter/portfilter.go b/plugins/portfilter/portfilter.go index b6e0fe0..c4301b6 100644 --- a/plugins/portfilter/portfilter.go +++ b/plugins/portfilter/portfilter.go @@ -78,7 +78,22 @@ func (p *PortFilterPlugin) Handle(r *http.Request) error { // Check port against blacklist for _, blacklistedPort := range p.portBlacklist { - if int(blacklistedPort.(int)) == port { + var bp int + switch v := blacklistedPort.(type) { + case int: + bp = v + case float64: + bp = int(v) + case string: + var err error + bp, err = strconv.Atoi(v) + if err != nil { + continue + } + default: + continue + } + if bp == port { req, err := database.NewRequest(r, "portblacklist") if err != nil { print("ERROR") @@ -95,7 +110,22 @@ func (p *PortFilterPlugin) Handle(r *http.Request) error { if len(p.portWhitelist) > 0 { isWhitelisted := false for _, whitelistedPort := range p.portWhitelist { - if int(whitelistedPort.(int)) == port { + var wp int + switch v := whitelistedPort.(type) { + case int: + wp = v + case float64: + wp = int(v) + case string: + var err error + wp, err = strconv.Atoi(v) + if err != nil { + continue + } + default: + continue + } + if wp == port { req, err := database.NewRequest(r, "portwhitelist") if err != nil { diff --git a/plugins/portfilter/portfilter_test.go b/plugins/portfilter/portfilter_test.go index 08680d2..6e2aedd 100644 --- a/plugins/portfilter/portfilter_test.go +++ b/plugins/portfilter/portfilter_test.go @@ -71,4 +71,18 @@ func TestPortFilterPlugin(t *testing.T) { if err == nil || err.Error() != "port is not whitelisted" { t.Errorf("PortFilterPlugin did not block non-whitelisted port. Error: %v", err) } + + // Test 5: Float64 ports from JSON + settings = map[string]interface{}{ + "port-blacklist": []interface{}{float64(8080)}, + "port-whitelist": []interface{}{}, + "active_mode": true, + } + plugin = NewPortFilterPlugin(settings, database.NewMock()).(*PortFilterPlugin) + req = httptest.NewRequest(http.MethodGet, "/", nil) + req.Host = "localhost:8080" + err = plugin.Handle(req) + if err == nil || err.Error() != "port is blacklisted" { + t.Errorf("PortFilterPlugin failed to handle float64 port. Error: %v", err) + } } diff --git a/run.sh b/run.sh index 7aa5c71..6754b0f 100755 --- a/run.sh +++ b/run.sh @@ -1,2 +1,2 @@ #! /bin/bash -go run cmd/main.go \ No newline at end of file +go run cmd/main.go