Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"publishers": [
{
"type": "slack",
"token": "xoxb-198202255696-5682091092327-m8IHyjQEnO6FdIIslpzjq2nz",
"token": "YOUR_SLACK_TOKEN",
"channelID": "C5UTW0J6N"
}
]
Expand Down
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
84 changes: 75 additions & 9 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"io"
"log"
"os"
"path/filepath"
"sync"

"github.com/fsnotify/fsnotify"

Check failure on line 11 in pkg/config/config.go

View workflow job for this annotation

GitHub Actions / Build

missing go.sum entry for module providing package github.com/fsnotify/fsnotify (imported by shadowguard/pkg/config); to add:
)

// PluginConfig represents the configuration for a single plugin
Expand Down Expand Up @@ -37,6 +41,69 @@
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.
Expand All @@ -47,18 +114,17 @@
}

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
}
34 changes: 32 additions & 2 deletions plugins/portfilter/portfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions plugins/portfilter/portfilter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 1 addition & 1 deletion run.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#! /bin/bash
go run cmd/main.go
go run cmd/main.go
Loading