diff --git a/Makefile b/Makefile index 733c7cd..7fa092e 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ GREEN := \033[0;32m PURPLE := \033[0;35m RESET := \033[0m RED := \033[0;31m -JACFARM_API_KEY := $(shell openssl rand -hex 32) +JACFARM_API_KEY := $(shell openssl rand -hex 16) ADMIN_PASS := $(shell openssl rand -hex 8) diff --git a/README.md b/README.md index 6d49500..e2c6c89 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@

CI Status - Codecov Coverage Status Release

@@ -46,6 +45,7 @@ make clean-all ## Features - Uploading exploits in ui +- Uploading exploits in cli - Real-time configuration farm options like number of concurrently running exploits, the size of the flag sending batch, team ip addresses, etc - The ability to [change the plugin for sending flags to jury](./docs/flag_sender/flag_sender.md). - There are already two sending plugins: [forcad_http](./workers/flag_sender/plugins/forcad_http/client.go) and [saarctf_tcp](./workers/flag_sender/plugins/saarctf_tcp/client.go). @@ -61,7 +61,7 @@ make clean-all ### Client -- **Frontend** - ui for +- **Frontend** (./jacfarm-frontend) - ui for - viewing flags with any filters - adding exploits of different types via '+' button - deleting or updating exploits by right mouse button @@ -71,14 +71,14 @@ make clean-all ![](./docs/img/frontend.png) -- **start_exploit.py** - python cli tool for starting exploits on local machine (TODO) +- **CLI** (./cli) - tool for starting exploits on local machine ### Server -- **Exploit Runner** - a worker that launches exploits on all teams. [More details](./docs/exploit_runner/exploit_runner.md) -- **Flag Sender** - a worker that sends flags to jury using *Plugins*. [More details](./docs/flag_sender/flag_sender.md) -- **JacFARM API** - API for frontend and cli start_exploit.py. -- **Config Loader** - loads config into db from config.yml on start. Next configuration editing is available through the frontend. +- **Exploit Runner** (./workers/exploit_runner) - a worker that launches exploits on all teams. [More details](./docs/exploit_runner/exploit_runner.md) +- **Flag Sender** (./workers/flag_sender) - a worker that sends flags to jury using *Plugins*. [More details](./docs/flag_sender/flag_sender.md) +- **JacFARM API** (./jacfarm-api) - API for frontend and cli start_exploit.py. +- **Config Loader** (./workers/config_loader) - loads config into db from config.yml on start. Next configuration editing is available through the frontend. #### Plugins @@ -90,4 +90,4 @@ make clean-all ### Arch Diagram -![](./docs/img/diagram.jpg) \ No newline at end of file +![](./docs/img/diagram.png) \ No newline at end of file diff --git a/cli/cmd/cli/main.go b/cli/cmd/cli/main.go new file mode 100644 index 0000000..eeb5b8f --- /dev/null +++ b/cli/cmd/cli/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "flag" + "fmt" + "jacfarmcli/internal/cli" + "log/slog" + "os" + "os/signal" + "syscall" + + "github.com/jacute/prettylogger" +) + +func main() { + args, err := cli.ParseArgs() + if err != nil { + flag.Usage() + os.Exit(1) + } + err = cli.ValidateArgs(args) + if err != nil { + fmt.Println(err.Error()) + flag.Usage() + os.Exit(2) + } + + log := slog.New(prettylogger.NewColoredHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelInfo, + })) + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + log.Info("running script") + err = cli.Run(args, log) + if err != nil { + os.Exit(2) + } + + sign := <-sigCh + + log.Info("stopping script", slog.String("signal", sign.String())) +} diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 0000000..bc0e2e0 --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,19 @@ +module jacfarmcli + +go 1.25.4 + +require ( + github.com/jacute/prettylogger v0.0.7 + github.com/stretchr/testify v1.11.1 + go.uber.org/mock v0.6.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/sys v0.18.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 0000000..2bb0d07 --- /dev/null +++ b/cli/go.sum @@ -0,0 +1,25 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/jacute/prettylogger v0.0.7 h1:inKCDEJ42j31hNVB6wAYZWOrc7E4QJ//x2hcR0LRhrg= +github.com/jacute/prettylogger v0.0.7/go.mod h1:3lynOiaGfyYdX6g8mz6cEg9CyLBZSTnPWwXdeQlao2w= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/internal/cli/cli.go b/cli/internal/cli/cli.go new file mode 100644 index 0000000..7c77d35 --- /dev/null +++ b/cli/internal/cli/cli.go @@ -0,0 +1,103 @@ +package cli + +import ( + "errors" + "flag" + "fmt" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" + "jacfarmcli/internal/worker" + "log/slog" + "os" + "time" + + "github.com/jacute/prettylogger" +) + +var ( + ErrUsage = errors.New("usage error") +) + +type Args struct { + Addr string + FlagRe string + Token string + Port int + ExecutablePath string + Timeout int + AttackPeriod int + MaxConcurrentExploits int +} + +func ParseArgs() (*Args, error) { + var args Args + + flag.IntVar(&args.Timeout, "t", 5, "timeout for http client (in seconds)") + flag.StringVar(&args.Token, "a", "", "JacFARM auth token") + flag.IntVar(&args.AttackPeriod, "period", 5, "attack period (in seconds)") + flag.IntVar(&args.MaxConcurrentExploits, "c", 50, "max concurrent exploits in one time") + flag.IntVar(&args.Port, "p", 15050, "jacfarm port") + flag.StringVar(&args.FlagRe, "f", "[A-Z0-9]{31}=", "flag regex") + + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage:\n") + fmt.Fprintf(os.Stderr, " %s [flags] \n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Example:\n") + fmt.Fprintf(os.Stderr, " %s -a TOKEN -p 15050 localhost ./exploit\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Flags:\n") + flag.PrintDefaults() + } + flag.Parse() + + if args.Token == "" { + return nil, flag.ErrHelp + } + + rest := flag.Args() + if len(rest) < 1 { + return nil, flag.ErrHelp + } + + args.Addr = rest[0] + args.ExecutablePath = rest[1] + + return &args, nil +} + +func ValidateArgs(args *Args) error { + if _, err := os.Stat(args.ExecutablePath); os.IsNotExist(err) { + return err + } + return nil +} + +// Run starts the worker +// Non-block function +func Run(args *Args, log *slog.Logger) error { + const op = "cli.Run" + + client, err := jacfarm_client.New( + args.Addr, + args.Token, + jacfarm_client.WithCustomPort(args.Port), + jacfarm_client.WithTimeout(time.Duration(args.Timeout)*time.Second), + ) + if err != nil { + log.Error("error creating jacfarm client", prettylogger.Err(err)) + return fmt.Errorf("%s: error creating jacfarm client %w", op, err) + } + w, err := worker.New( + client, log, + args.ExecutablePath, + args.FlagRe, + worker.WithAttackPeriod(time.Duration(args.AttackPeriod)*time.Second), + worker.WithMaxConcurrentExploits(args.MaxConcurrentExploits), + ) + if err != nil { + log.Error("error creating worker", prettylogger.Err(err)) + return fmt.Errorf("%s: error creating worker %e", op, err) + } + + w.Run() + + return nil +} diff --git a/cli/internal/clients/jacfarm/client.go b/cli/internal/clients/jacfarm/client.go new file mode 100644 index 0000000..2a5fff8 --- /dev/null +++ b/cli/internal/clients/jacfarm/client.go @@ -0,0 +1,203 @@ +package jacfarm_client + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "jacfarmcli/pkg/common_config" + "net/http" + "time" +) + +var ( + ErrFlagFormatNotFound = errors.New("flag format not found") + ErrAuth = errors.New("auth error") +) + +const defaultPort = 15050 +const defaultTimeout = 5 * time.Second + +type Client struct { + baseURL string + httpClient *http.Client + token string +} + +type options struct { + port *int + timeout time.Duration // int seconds +} + +type Option func(opts *options) error + +func New(host string, token string, opts ...Option) (*Client, error) { + options := &options{} + for _, opt := range opts { + err := opt(options) + if err != nil { + return nil, err + } + } + + // default params + port := defaultPort + timeout := defaultTimeout + + // apply options if use + if options.port != nil { + port = *options.port + } + if options.timeout != 0 { + timeout = options.timeout + } + + c := &Client{ + baseURL: fmt.Sprintf("http://%s:%d/jacfarm-api", host, port), + httpClient: &http.Client{ + Timeout: timeout, + }, + token: token, + } + + // trying to ping + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, err := c.GetTeams(ctx) + if err != nil { + if errors.Is(err, ErrAuth) { + return nil, fmt.Errorf("auth error, check token") + } + return nil, err + } + + return c, nil +} + +func WithTimeout(timeout time.Duration) Option { + return func(opts *options) error { + opts.timeout = timeout + return nil + } +} + +func WithCustomPort(port int) Option { + return func(opts *options) error { + p := port + opts.port = &p + return nil + } +} + +func (c *Client) GetTeams(ctx context.Context) ([]*Team, error) { + url := fmt.Sprintf("%s/api/v1/service/teams", c.baseURL) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", c.token) + + res, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode == 401 { + return nil, ErrAuth + } + if res.StatusCode != 200 { + return nil, fmt.Errorf("incorrect status code: %d", res.StatusCode) + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var resBody *ListTeamsResponse + if err := json.Unmarshal(data, &resBody); err != nil { + return nil, err + } + + return resBody.Teams, nil +} + +func (c *Client) getConfig(ctx context.Context) ([]*Config, error) { + url := fmt.Sprintf("%s/api/v1/service/config", c.baseURL) + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", c.token) + + res, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return nil, fmt.Errorf("incorrect status code: %d", res.StatusCode) + } + + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + var cfg []*Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +func (c *Client) GetFlagFormat(ctx context.Context) (string, error) { + cfg, err := c.getConfig(ctx) + if err != nil { + return "", err + } + + for _, cfgRow := range cfg { + if cfgRow.Name == common_config.ConfigFlagFormatKey { + return cfgRow.Value, nil + } + } + + return "", ErrFlagFormatNotFound +} + +func (c *Client) SendFlags(ctx context.Context, flags []*ServiceFlag) error { + url := fmt.Sprintf("%s/api/v1/service/flags", c.baseURL) + req, err := http.NewRequestWithContext(ctx, "POST", url, nil) + if err != nil { + return err + } + req.Header.Set("Authorization", c.token) + req.Header.Set("Content-Type", "application/json") + + data, err := json.Marshal(&ServicePutFlagRequest{ + Flags: flags, + }) + if err != nil { + return err + } + + req.Body = io.NopCloser(bytes.NewReader(data)) + + res, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != 200 { + return fmt.Errorf("incorrect status code: %d", res.StatusCode) + } + + return nil +} diff --git a/cli/internal/clients/jacfarm/dto.go b/cli/internal/clients/jacfarm/dto.go new file mode 100644 index 0000000..954c0e8 --- /dev/null +++ b/cli/internal/clients/jacfarm/dto.go @@ -0,0 +1,41 @@ +package jacfarm_client + +import "net" + +type Config struct { + ID int64 `json:"id"` + Name string `json:"name"` + Value string `json:"value"` +} + +type Response struct { + Error string `json:"error,omitempty"` + Status string `json:"status"` +} + +type Team struct { + ID int64 `json:"id"` + Name string `json:"name"` + IP net.IP `json:"ip"` +} + +type ListTeamsResponse struct { + *Response + Teams []*Team `json:"teams"` + Count int `json:"count"` +} + +type GetConfigResponse struct { + *Response + Config []*Config `json:"config"` + Count int `json:"count"` +} + +type ServicePutFlagRequest struct { + Flags []*ServiceFlag `json:"flags"` +} + +type ServiceFlag struct { + Flag string `json:"flag"` + TeamID int64 `json:"team_id"` +} diff --git a/cli/internal/worker/executor.go b/cli/internal/worker/executor.go new file mode 100644 index 0000000..22151a2 --- /dev/null +++ b/cli/internal/worker/executor.go @@ -0,0 +1,49 @@ +package worker + +import ( + "context" + "log/slog" + "time" + + "github.com/jacute/prettylogger" +) + +func (w *Worker) runExecutor() { + const op = "worker.startExecutor" + log := w.log.With(slog.String("op", op)) + + timer := time.NewTimer(w.attackPeriod) + defer timer.Stop() + + for { + select { + case <-w.stopCh: + return + case <-timer.C: + log.Info("starting attack") + ctx, cancel := context.WithTimeout(context.Background(), w.attackPeriod) + defer cancel() + + teams, err := w.client.GetTeams(ctx) + if err != nil { + log.Error( + "error getting teams", + prettylogger.Err(err), + ) + timer.Reset(w.attackPeriod) + continue + } + + err = w.attackAll(ctx, teams) + if err != nil { + log.Error( + "error attacking", + prettylogger.Err(err), + ) + timer.Reset(w.attackPeriod) + continue + } + timer.Reset(w.attackPeriod) + } + } +} diff --git a/cli/internal/worker/mocks/matcher.go b/cli/internal/worker/mocks/matcher.go new file mode 100644 index 0000000..50aca63 --- /dev/null +++ b/cli/internal/worker/mocks/matcher.go @@ -0,0 +1,58 @@ +package mocks + +import ( + "fmt" + "reflect" + + "go.uber.org/mock/gomock" +) + +type unorderedSliceMatcher struct { + want interface{} +} + +func (m unorderedSliceMatcher) Matches(x interface{}) bool { + wantVal := reflect.ValueOf(m.want) + gotVal := reflect.ValueOf(x) + + // оба должны быть слайсами + if wantVal.Kind() != reflect.Slice || gotVal.Kind() != reflect.Slice { + return false + } + + // длина должна совпадать + if wantVal.Len() != gotVal.Len() { + return false + } + + // карта счётчиков для элементов want + used := make([]bool, wantVal.Len()) + + // перебираем got и пытаемся найти deep-равный в want + for i := 0; i < gotVal.Len(); i++ { + match := false + for j := 0; j < wantVal.Len(); j++ { + if used[j] { + continue + } + if reflect.DeepEqual(gotVal.Index(i).Interface(), wantVal.Index(j).Interface()) { + used[j] = true + match = true + break + } + } + if !match { + return false + } + } + + return true +} + +func (m unorderedSliceMatcher) String() string { + return fmt.Sprintf("unordered slice %v", m.want) +} + +func UnorderedSlice(want interface{}) gomock.Matcher { + return unorderedSliceMatcher{want: want} +} diff --git a/cli/internal/worker/mocks/worker_mock.go b/cli/internal/worker/mocks/worker_mock.go new file mode 100644 index 0000000..da93724 --- /dev/null +++ b/cli/internal/worker/mocks/worker_mock.go @@ -0,0 +1,71 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: worker.go +// +// Generated by this command: +// +// mockgen -source=worker.go -destination=./mocks/worker_mock.go -package=mocks -mock_names=storage=WorkerMock Service +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + jacfarm "jacfarmcli/internal/clients/jacfarm" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockJacFARMClient is a mock of JacFARMClient interface. +type MockJacFARMClient struct { + ctrl *gomock.Controller + recorder *MockJacFARMClientMockRecorder + isgomock struct{} +} + +// MockJacFARMClientMockRecorder is the mock recorder for MockJacFARMClient. +type MockJacFARMClientMockRecorder struct { + mock *MockJacFARMClient +} + +// NewMockJacFARMClient creates a new mock instance. +func NewMockJacFARMClient(ctrl *gomock.Controller) *MockJacFARMClient { + mock := &MockJacFARMClient{ctrl: ctrl} + mock.recorder = &MockJacFARMClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockJacFARMClient) EXPECT() *MockJacFARMClientMockRecorder { + return m.recorder +} + +// GetTeams mocks base method. +func (m *MockJacFARMClient) GetTeams(ctx context.Context) ([]*jacfarm.Team, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetTeams", ctx) + ret0, _ := ret[0].([]*jacfarm.Team) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetTeams indicates an expected call of GetTeams. +func (mr *MockJacFARMClientMockRecorder) GetTeams(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTeams", reflect.TypeOf((*MockJacFARMClient)(nil).GetTeams), ctx) +} + +// SendFlags mocks base method. +func (m *MockJacFARMClient) SendFlags(ctx context.Context, flags []*jacfarm.ServiceFlag) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendFlags", ctx, flags) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendFlags indicates an expected call of SendFlags. +func (mr *MockJacFARMClientMockRecorder) SendFlags(ctx, flags any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendFlags", reflect.TypeOf((*MockJacFARMClient)(nil).SendFlags), ctx, flags) +} diff --git a/cli/internal/worker/sender.go b/cli/internal/worker/sender.go new file mode 100644 index 0000000..204202f --- /dev/null +++ b/cli/internal/worker/sender.go @@ -0,0 +1,66 @@ +package worker + +import ( + "context" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" + "log/slog" + "time" + + "github.com/jacute/prettylogger" +) + +const senderSize = 50 +const sendPeriod = 5 * time.Second + +func (w *Worker) runSender() { + const op = "worker.startSender" + log := w.log.With(slog.String("op", op)) + + timer := time.NewTimer(sendPeriod) + defer timer.Stop() + flagBuffer := make([]*jacfarm_client.ServiceFlag, 0, senderSize) + + for { + select { + case <-w.stopCh: + for { + select { + case flags := <-w.flagQueue: + flagBuffer = append(flagBuffer, flags...) + default: + if len(flagBuffer) > 0 { + err := w.client.SendFlags(context.Background(), flagBuffer) + if err != nil { + log.Error("error sending flags", prettylogger.Err(err)) + } else { + log.Info("flags send successfully", slog.Int("flags_count", len(flagBuffer)), slog.Any("first_flags", flagBuffer[:5])) + } + } + return + } + } + case <-timer.C: + if len(flagBuffer) > 0 { + err := w.client.SendFlags(context.Background(), flagBuffer) + if err != nil { + log.Error("error sending flags", prettylogger.Err(err)) + } else { + log.Info("flags send successfully", slog.Int("flags_count", len(flagBuffer)), slog.Any("first_flags", flagBuffer[:5])) + } + flagBuffer = flagBuffer[:0] + } + timer.Reset(sendPeriod) + case flags := <-w.flagQueue: + flagBuffer = append(flagBuffer, flags...) + if len(flagBuffer) >= senderSize { + err := w.client.SendFlags(context.Background(), flagBuffer) + if err != nil { + log.Error("error sending flags", prettylogger.Err(err)) + } else { + log.Info("flags send successfully", slog.Int("flags_count", len(flagBuffer)), slog.Any("first_flags", flagBuffer[:5])) + } + flagBuffer = flagBuffer[:0] + } + } + } +} diff --git a/cli/internal/worker/testcases/testsploit.sh b/cli/internal/worker/testcases/testsploit.sh new file mode 100755 index 0000000..0c3065e --- /dev/null +++ b/cli/internal/worker/testcases/testsploit.sh @@ -0,0 +1,6 @@ +#!/bin/bash + + +echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' +echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=' +echo 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=' diff --git a/cli/internal/worker/worker.go b/cli/internal/worker/worker.go new file mode 100644 index 0000000..e9ec0cc --- /dev/null +++ b/cli/internal/worker/worker.go @@ -0,0 +1,212 @@ +package worker + +import ( + "context" + "errors" + "fmt" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" + "log/slog" + "os" + "os/exec" + "regexp" + "sync" + "time" + + "github.com/jacute/prettylogger" +) + +var ( + ErrExploitNotExecutable = errors.New("exploit is not executable") + ErrExploitNotFile = errors.New("exploit is not file") +) + +const ( + defaultAttackPeriod = 5 * time.Second + defaultMaxConcurrentExploits = 5 +) + +//go:generate mockgen -source=worker.go -destination=./mocks/worker_mock.go -package=mocks -mock_names=storage=WorkerMock Service +type JacFARMClient interface { + GetTeams(ctx context.Context) ([]*jacfarm_client.Team, error) + SendFlags(ctx context.Context, flags []*jacfarm_client.ServiceFlag) error +} + +type Worker struct { + client JacFARMClient + attackPeriod time.Duration + maxConcurrentExploits int + exploitPath string + flagRe *regexp.Regexp + + log *slog.Logger + stopCh chan struct{} + flagQueue chan []*jacfarm_client.ServiceFlag +} + +type options struct { + attackPeriod *time.Duration + maxConcurrentExploits *int +} + +type Option func(opts *options) error + +func New( + client JacFARMClient, + log *slog.Logger, + exploitPath string, + flagRegexp string, + opts ...Option, +) (*Worker, error) { + workerOpts := &options{} + for _, opt := range opts { + err := opt(workerOpts) + if err != nil { + return nil, err + } + } + + flagRe, err := regexp.Compile(flagRegexp) + if err != nil { + return nil, err + } + + w := &Worker{ + client: client, + attackPeriod: defaultAttackPeriod, + maxConcurrentExploits: defaultMaxConcurrentExploits, + exploitPath: exploitPath, + flagRe: flagRe, + + log: log, + stopCh: make(chan struct{}), + flagQueue: make(chan []*jacfarm_client.ServiceFlag, senderSize), // TODO: optimization of buf size + } + + if workerOpts.attackPeriod != nil { + w.attackPeriod = *workerOpts.attackPeriod + } + if workerOpts.maxConcurrentExploits != nil { + w.maxConcurrentExploits = *workerOpts.maxConcurrentExploits + } + + return w, nil +} + +func WithAttackPeriod(attackPeriod time.Duration) Option { + return func(opts *options) error { + opts.attackPeriod = &attackPeriod + return nil + } +} + +func WithMaxConcurrentExploits(count int) Option { + return func(opts *options) error { + opts.maxConcurrentExploits = &count + return nil + } +} + +// Run starts the worker. It is a non-blocking function. +// It starts the receiver (sender) and the consumer (executor) in separate goroutines. +// The receiver (sender) is responsible for receiving flags from the JacFARM client and sending them to the consumer (executor). +// The consumer (executor) is responsible for executing the exploits and sending the result back to the JacFARM client. +// The function logs information about the worker when it starts, including the maximum number of concurrent exploits and the attack period. +func (w *Worker) Run() { + const op = "worker.Run" + log := w.log.With(slog.String("op", op)) + log.Info( + "starting worker", + slog.Int("max_concurrent_exploits", w.maxConcurrentExploits), + slog.Duration("attack_period", w.attackPeriod), + ) + + // start receiver (sender) + go w.runSender() + // start consumer (executor) + go w.runExecutor() +} + +func (w *Worker) attackAll( + ctx context.Context, + teams []*jacfarm_client.Team, +) error { + const op = "worker.attackAll" + log := w.log.With(slog.String("op", op), slog.String("exploit_path", w.exploitPath)) + + info, err := os.Stat(w.exploitPath) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("exploit %s does not exist", w.exploitPath) + } + return err + } + if info.IsDir() { + return fmt.Errorf("exploit %s is not file", w.exploitPath) + } + perms := info.Mode().Perm() + isExec := perms&0111 != 0 + if !isExec { + return fmt.Errorf("exploit %s is not executable", w.exploitPath) + } + + concurrentCh := make(chan struct{}, w.maxConcurrentExploits) + wg := &sync.WaitGroup{} + wg.Add(len(teams)) + + for _, t := range teams { + concurrentCh <- struct{}{} + go func() { + defer func() { + wg.Done() + <-concurrentCh + }() + out, err := attack(ctx, w.exploitPath, t.IP.String()) + if err != nil { + log.Error( + "error attacking team", + prettylogger.Err(err), + slog.String("team_ip", t.IP.String()), + ) + return + } + flags := parseFlags(out, w.flagRe) + for _, f := range flags { + w.flagQueue <- []*jacfarm_client.ServiceFlag{ + { + Flag: f, + TeamID: t.ID, + }, + } + } + }() + } + + wg.Wait() + close(concurrentCh) + return nil +} + +func attack(ctx context.Context, exploitPath, targetIP string) (exploitOut []byte, err error) { + cmd := exec.CommandContext(ctx, exploitPath, targetIP) + out, err := cmd.Output() + if err != nil { + return nil, err + } + + return out, err +} + +func parseFlags(text []byte, re *regexp.Regexp) []string { + flags := make([]string, 0) + bts := re.FindAll(text, -1) + for _, b := range bts { + flags = append(flags, string(b)) + } + return flags +} + +func (w *Worker) Stop() { + const op = "worker.Stop" + w.log.Info("stopping worker") + close(w.stopCh) +} diff --git a/cli/internal/worker/worker_test.go b/cli/internal/worker/worker_test.go new file mode 100644 index 0000000..f54f2dc --- /dev/null +++ b/cli/internal/worker/worker_test.go @@ -0,0 +1,127 @@ +package worker + +import ( + "context" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" + "jacfarmcli/internal/worker/mocks" + "log/slog" + "net" + "os" + "regexp" + "testing" + "time" + + "github.com/jacute/prettylogger" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestAttack(t *testing.T) { + const testPath = "testcases/testsploit.sh" + + if _, err := os.Stat(testPath); os.IsNotExist(err) { + t.Fatal(err) + } + + if err := os.Chmod(testPath, 0744); err != nil { + t.Fatal(err) + } + + out, err := attack(context.Background(), testPath, "127.0.0.1") + require.NoError(t, err) + + flags := parseFlags(out, regexp.MustCompile("[A-Z0-9]{31}=")) + require.Equal(t, []string{ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + }, flags) +} + +func TestAttackAll(t *testing.T) { + testcases := []struct { + name string + flags []string + worker func() (*Worker, *gomock.Controller) + }{ + { + name: "ok", + flags: []string{ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + }, + worker: func() (*Worker, *gomock.Controller) { + ctrl := gomock.NewController(t) + clientMock := mocks.NewMockJacFARMClient(ctrl) + clientMock.EXPECT().GetTeams(gomock.Any()).Return([]*jacfarm_client.Team{ + { + ID: 1, + Name: "aboba", + IP: net.ParseIP("1.1.1.1"), + }, + { + ID: 2, + Name: "aboba2", + IP: net.ParseIP("1.1.1.2"), + }, + }, nil).Times(1) + clientMock.EXPECT().SendFlags(gomock.Any(), mocks.UnorderedSlice([]*jacfarm_client.ServiceFlag{ + { + Flag: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + TeamID: 1, + }, + { + Flag: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + TeamID: 1, + }, + { + Flag: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + TeamID: 1, + }, + { + Flag: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + TeamID: 2, + }, + { + Flag: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + TeamID: 2, + }, + { + Flag: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + TeamID: 2, + }, + })).Return(nil).Times(1) + w, err := New( + clientMock, + slog.New(prettylogger.NewDiscardHandler()), + "testcases/testsploit.sh", + "[A-Z0-9]{31}=", + WithAttackPeriod(1*time.Second), + ) + if err != nil { + t.Fatal(err) + } + + return w, ctrl + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + w, ctrl := tc.worker() + w.Run() + time.Sleep(1500 * time.Millisecond) + w.Stop() + time.Sleep(1 * time.Second) + ctrl.Finish() + }) + } +} diff --git a/cli/pkg/common_config/config.go b/cli/pkg/common_config/config.go new file mode 100644 index 0000000..ba98f56 --- /dev/null +++ b/cli/pkg/common_config/config.go @@ -0,0 +1,16 @@ +package common_config + +const ( + ConfigFlagFormatKey = "EXPLOIT_RUNNER_FLAG_FORMAT" + ConfigExploitDuration = "EXPLOIT_RUNNER_PERIOD" + ConfigExploitMaxWorkingTime = "EXPLOIT_RUNNER_MAX_WORKING_TIME" + ConfigMaxConcurrentExploits = "EXPLOIT_RUNNER_MAX_CONCURRENT_EXPLOITS" + ConfigFlagSenderPlugin = "FLAG_SENDER_PLUGIN" + ConfigFlagSenderSubmitTimeout = "FLAG_SENDER_SUBMIT_TIMEOUT" + ConfigFlagSenderSubmitPeriod = "FLAG_SENDER_SUBMIT_PERIOD" + ConfigFlagSenderJuryFlagURL = "FLAG_SENDER_JURY_FLAG_URL_OR_HOST" + ConfigFlagSenderToken = "FLAG_SENDER_TOKEN" + ConfigFlagSenderSubmitLimit = "FLAG_SENDER_SUBMIT_LIMIT" + ConfigFlagSenderFlagTTL = "FLAG_SENDER_FLAG_TTL" + ConfigVenvMaxInstallTime = "EXPLOIT_RUNNER_VENV_MAX_INSTALL_TIME" +) diff --git a/docs/diagram.drawio b/docs/diagram.drawio index e7d2109..2bc7ef5 100644 --- a/docs/diagram.drawio +++ b/docs/diagram.drawio @@ -1,148 +1,143 @@ - - - + + + - - + + - - + + - + - - + + - - + + - + - - + + - - + + - - + + - - + + - + - - + + - - + + - - + + - - + + - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - - + + - - + + - - - - + + - - + + - - - - + - - + + - - + + - + - - + + - + - - + + - + @@ -151,33 +146,45 @@ - - + + - - + + - + - - + + - - + + - - + + + + + + + + + + + + + + diff --git a/docs/img/diagram.jpg b/docs/img/diagram.jpg deleted file mode 100644 index fc361bd..0000000 Binary files a/docs/img/diagram.jpg and /dev/null differ diff --git a/docs/img/diagram.png b/docs/img/diagram.png new file mode 100644 index 0000000..7d3d682 Binary files /dev/null and b/docs/img/diagram.png differ diff --git a/jacfarm-api/internal/http/dto/service.go b/jacfarm-api/internal/http/dto/service.go new file mode 100644 index 0000000..d1d6bd1 --- /dev/null +++ b/jacfarm-api/internal/http/dto/service.go @@ -0,0 +1,10 @@ +package dto + +type ServicePutFlagRequest struct { + Flags []*ServiceFlag `json:"flags"` +} + +type ServiceFlag struct { + Flag string `json:"flag"` + TeamID int64 `json:"team_id"` +} diff --git a/jacfarm-api/internal/http/handlers/handlers.go b/jacfarm-api/internal/http/handlers/handlers.go index 5d01d80..87c49c3 100644 --- a/jacfarm-api/internal/http/handlers/handlers.go +++ b/jacfarm-api/internal/http/handlers/handlers.go @@ -10,6 +10,7 @@ import ( type Service interface { ListFlags(ctx context.Context, filter *dto.ListFlagsFilter) ([]*models.FlagEnrich, int, error) PutFlag(ctx context.Context, flag string) error + ServicePutFlag(ctx context.Context, req *dto.ServicePutFlagRequest) error GetFlagsCount(ctx context.Context) (int, error) ListExploits(ctx context.Context, filter *dto.ListExploitsFilter) ([]*models.Exploit, int, error) diff --git a/jacfarm-api/internal/http/handlers/mocks/service_mock.go b/jacfarm-api/internal/http/handlers/mocks/service_mock.go index d71c534..6ddd0df 100644 --- a/jacfarm-api/internal/http/handlers/mocks/service_mock.go +++ b/jacfarm-api/internal/http/handlers/mocks/service_mock.go @@ -271,6 +271,20 @@ func (mr *MockServiceMockRecorder) PutFlag(ctx, flag any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutFlag", reflect.TypeOf((*MockService)(nil).PutFlag), ctx, flag) } +// ServicePutFlag mocks base method. +func (m *MockService) ServicePutFlag(ctx context.Context, req *dto.ServicePutFlagRequest) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ServicePutFlag", ctx, req) + ret0, _ := ret[0].(error) + return ret0 +} + +// ServicePutFlag indicates an expected call of ServicePutFlag. +func (mr *MockServiceMockRecorder) ServicePutFlag(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServicePutFlag", reflect.TypeOf((*MockService)(nil).ServicePutFlag), ctx, req) +} + // ToggleExploit mocks base method. func (m *MockService) ToggleExploit(ctx context.Context, id string) (bool, error) { m.ctrl.T.Helper() diff --git a/jacfarm-api/internal/http/handlers/service.go b/jacfarm-api/internal/http/handlers/service.go new file mode 100644 index 0000000..c11a719 --- /dev/null +++ b/jacfarm-api/internal/http/handlers/service.go @@ -0,0 +1,27 @@ +package handlers + +import ( + "JacFARM/internal/http/dto" + + "github.com/gofiber/fiber/v3" +) + +func (h *Handlers) ServicePutFlag() func(c fiber.Ctx) error { + return func(c fiber.Ctx) error { + if c.Get("Content-Type") != "application/json" { + return c.Status(fiber.StatusBadRequest).JSON(dto.ErrInvalidContentType) + } + + var req dto.ServicePutFlagRequest + if err := c.Bind().Body(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(dto.ErrDecodingBody) + } + + err := h.service.ServicePutFlag(c.RequestCtx(), &req) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(dto.ErrInternal) + } + + return c.JSON(dto.OK()) + } +} diff --git a/jacfarm-api/internal/http/server/config_test.go b/jacfarm-api/internal/http/server/config_test.go index 61a18bd..28941aa 100644 --- a/jacfarm-api/internal/http/server/config_test.go +++ b/jacfarm-api/internal/http/server/config_test.go @@ -32,8 +32,8 @@ func TestGetConfig(t *testing.T) { { name: "ok", queryParams: map[string][]string{ - "limit": []string{"10"}, - "page": []string{"1"}, + "limit": {"10"}, + "page": {"1"}, }, expectedStatusCode: http.StatusOK, mock: func() *mocks.MockService { @@ -59,8 +59,8 @@ func TestGetConfig(t *testing.T) { { name: "service error", queryParams: map[string][]string{ - "limit": []string{"10"}, - "page": []string{"1"}, + "limit": {"10"}, + "page": {"1"}, }, expectedStatusCode: http.StatusInternalServerError, mock: func() *mocks.MockService { @@ -74,8 +74,8 @@ func TestGetConfig(t *testing.T) { { name: "negative page error", queryParams: map[string][]string{ - "limit": []string{"10"}, - "page": []string{"-1"}, + "limit": {"10"}, + "page": {"-1"}, }, expectedStatusCode: http.StatusBadRequest, mock: func() *mocks.MockService { @@ -87,8 +87,8 @@ func TestGetConfig(t *testing.T) { { name: "negative limit error", queryParams: map[string][]string{ - "limit": []string{"-10"}, - "page": []string{"1"}, + "limit": {"-10"}, + "page": {"1"}, }, expectedStatusCode: http.StatusBadRequest, mock: func() *mocks.MockService { @@ -100,8 +100,8 @@ func TestGetConfig(t *testing.T) { { name: "incorrect type limit error", queryParams: map[string][]string{ - "limit": []string{"-das10"}, - "page": []string{"1"}, + "limit": {"-das10"}, + "page": {"1"}, }, expectedStatusCode: http.StatusBadRequest, mock: func() *mocks.MockService { @@ -113,8 +113,8 @@ func TestGetConfig(t *testing.T) { { name: "incorrect type page error", queryParams: map[string][]string{ - "limit": []string{"10"}, - "page": []string{"das"}, + "limit": {"10"}, + "page": {"das"}, }, expectedStatusCode: http.StatusBadRequest, mock: func() *mocks.MockService { @@ -135,7 +135,9 @@ func TestGetConfig(t *testing.T) { ) res, err := st.app.Test(req) - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() require.NoError(t, err) require.Equal(t, tc.expectedStatusCode, res.StatusCode) @@ -272,7 +274,9 @@ func TestUpdateConfig(t *testing.T) { } res, err := st.app.Test(req) - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() require.NoError(t, err) data, err := io.ReadAll(res.Body) diff --git a/jacfarm-api/internal/http/server/exploits_test.go b/jacfarm-api/internal/http/server/exploits_test.go index 04ec47d..2e54a7b 100644 --- a/jacfarm-api/internal/http/server/exploits_test.go +++ b/jacfarm-api/internal/http/server/exploits_test.go @@ -145,7 +145,9 @@ func TestListExploits(t *testing.T) { ) res, err := st.app.Test(req) - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() require.NoError(t, err) require.Equal(t, tc.expectedStatusCode, res.StatusCode) @@ -221,7 +223,9 @@ func TestListShortExploits(t *testing.T) { ) res, err := st.app.Test(req) - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() require.NoError(t, err) require.Equal(t, tc.expectedStatusCode, res.StatusCode) @@ -472,7 +476,9 @@ func TestUploadExploit(t *testing.T) { req.Header.Add("Content-Type", contentType) res, err := st.app.Test(req) - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() require.NoError(t, err) assert.Equal(t, tc.expectedStatusCode, res.StatusCode, tc.name) @@ -573,7 +579,9 @@ func TestToggleExploit(t *testing.T) { ) res, err := st.app.Test(req) - defer res.Body.Close() + defer func() { + _ = res.Body.Close() + }() require.NoError(t, err) require.Equal(t, tc.expectedStatusCode, res.StatusCode) diff --git a/jacfarm-api/internal/http/server/router.go b/jacfarm-api/internal/http/server/router.go index a5bab66..42a8b02 100644 --- a/jacfarm-api/internal/http/server/router.go +++ b/jacfarm-api/internal/http/server/router.go @@ -75,6 +75,8 @@ func setupRouter(h *handlers.Handlers, cfg *config.HTTPConfig, apiKey string) *f serviceGroup := apiV1.Group("/service") serviceGroup.Post("/flags", middlewares.ServiceAuthMiddleware(apiKey), h.PutFlag()) + serviceGroup.Get("/teams", middlewares.ServiceAuthMiddleware(apiKey), h.ListTeams()) + serviceGroup.Get("/config", middlewares.ServiceAuthMiddleware(apiKey), h.GetConfig()) return r } diff --git a/jacfarm-api/internal/rabbitmq/flags_queue.go b/jacfarm-api/internal/rabbitmq/flags_queue.go index 03cdcda..46e3f41 100644 --- a/jacfarm-api/internal/rabbitmq/flags_queue.go +++ b/jacfarm-api/internal/rabbitmq/flags_queue.go @@ -27,6 +27,9 @@ func (r *Rabbit) PublishFlag(flag *rabbitmq_dto.Flag) error { DeliveryMode: amqp.Persistent, ContentType: "application/json", Body: output, + Headers: amqp.Table{ + "x-deduplication-header": flag.Value, + }, }, ) if err != nil { diff --git a/jacfarm-api/internal/rabbitmq/rabbitmq.go b/jacfarm-api/internal/rabbitmq/rabbitmq.go index 35fc1f8..2d1d2cd 100644 --- a/jacfarm-api/internal/rabbitmq/rabbitmq.go +++ b/jacfarm-api/internal/rabbitmq/rabbitmq.go @@ -38,7 +38,8 @@ func New(cfg *config.RabbitMQConfig) *Rabbit { false, // exclusive false, // no-wait amqp.Table{ - "x-message-deduplication": true, + "x-deduplication-header": "x-deduplication-header", + "x-cache-ttl": int32(300000), // TTL в ms }, ) if err != nil { diff --git a/jacfarm-api/internal/service/jacfarm/exploits.go b/jacfarm-api/internal/service/jacfarm/exploits.go index 6bf0d3b..9455797 100644 --- a/jacfarm-api/internal/service/jacfarm/exploits.go +++ b/jacfarm-api/internal/service/jacfarm/exploits.go @@ -4,7 +4,7 @@ import ( "JacFARM/internal/http/dto" "JacFARM/internal/models" storage_errors "JacFARM/internal/storage" - "JacFARM/internal/utils" + "JacFARM/internal/zip" "context" "errors" "fmt" @@ -96,7 +96,7 @@ func (s *Service) UploadExploit(ctx context.Context, req *dto.UploadExploitReque return "", err } case models.ExploitTypePythonZip: - err = utils.SecureUnzip(req.File, exploitWorkDirPath, 200*1024*1024, 50*1024*1024) + err = zip.SecureUnzip(req.File, exploitWorkDirPath, 200*1024*1024, 50*1024*1024) if err != nil { return "", fmt.Errorf("%s: error unzipping %w", op, err) } diff --git a/jacfarm-api/internal/service/jacfarm/flags.go b/jacfarm-api/internal/service/jacfarm/flags.go index 9ad25a1..293f20e 100644 --- a/jacfarm-api/internal/service/jacfarm/flags.go +++ b/jacfarm-api/internal/service/jacfarm/flags.go @@ -45,6 +45,26 @@ func (s *Service) PutFlag(ctx context.Context, flag string) error { return nil } +func (s *Service) ServicePutFlag(ctx context.Context, req *dto.ServicePutFlagRequest) error { + const op = "service.jacfarm.ServicePutFlag" + log := s.log.With(slog.String("op", op)) + + for _, flag := range req.Flags { + err := s.que.PublishFlag(&rabbitmq_dto.Flag{ + Value: flag.Flag, + TeamID: flag.TeamID, + SourceType: rabbitmq_dto.LocalExploitSourceType, + CreatedAt: time.Now().UTC(), + }) + if err != nil { + log.Error("error sending flags to queue", slog.Any("flag", flag), prettylogger.Err(err)) + return err + } + } + + return nil +} + func (s *Service) GetFlagsCount(ctx context.Context) (int, error) { const op = "service.jacfarm.GetFlagsCount" log := s.log.With(slog.String("op", op)) diff --git a/jacfarm-api/internal/utils/testcases/001_file195KB.zip b/jacfarm-api/internal/zip/testcases/001_file195KB.zip similarity index 100% rename from jacfarm-api/internal/utils/testcases/001_file195KB.zip rename to jacfarm-api/internal/zip/testcases/001_file195KB.zip diff --git a/jacfarm-api/internal/utils/testcases/002_total_size_390KB.zip b/jacfarm-api/internal/zip/testcases/002_total_size_390KB.zip similarity index 100% rename from jacfarm-api/internal/utils/testcases/002_total_size_390KB.zip rename to jacfarm-api/internal/zip/testcases/002_total_size_390KB.zip diff --git a/jacfarm-api/internal/utils/testcases/003_path_traversal.zip b/jacfarm-api/internal/zip/testcases/003_path_traversal.zip similarity index 100% rename from jacfarm-api/internal/utils/testcases/003_path_traversal.zip rename to jacfarm-api/internal/zip/testcases/003_path_traversal.zip diff --git a/jacfarm-api/internal/utils/testcases/004_double_dot.zip b/jacfarm-api/internal/zip/testcases/004_double_dot.zip similarity index 100% rename from jacfarm-api/internal/utils/testcases/004_double_dot.zip rename to jacfarm-api/internal/zip/testcases/004_double_dot.zip diff --git a/jacfarm-api/internal/utils/utils.go b/jacfarm-api/internal/zip/zip.go similarity index 99% rename from jacfarm-api/internal/utils/utils.go rename to jacfarm-api/internal/zip/zip.go index 12c28c3..a14b398 100644 --- a/jacfarm-api/internal/utils/utils.go +++ b/jacfarm-api/internal/zip/zip.go @@ -1,4 +1,4 @@ -package utils +package zip import ( "archive/zip" diff --git a/jacfarm-api/internal/utils/utils_test.go b/jacfarm-api/internal/zip/zip_test.go similarity index 99% rename from jacfarm-api/internal/utils/utils_test.go rename to jacfarm-api/internal/zip/zip_test.go index c07ae84..e59a652 100644 --- a/jacfarm-api/internal/utils/utils_test.go +++ b/jacfarm-api/internal/zip/zip_test.go @@ -1,4 +1,4 @@ -package utils +package zip import ( "os" diff --git a/workers/config_loader/internal/service/config.go b/workers/config_loader/internal/service/config.go index 889d076..93e3e53 100644 --- a/workers/config_loader/internal/service/config.go +++ b/workers/config_loader/internal/service/config.go @@ -52,18 +52,20 @@ func (s *Service) LoadConfigIntoDB(ctx context.Context, cfg *config.Config) erro } } - // add ip from N - ips := utils.ExpandIpFromN( - cfg.ExploitRunner.TeamIPFromN.NStart, - cfg.ExploitRunner.TeamIPFromN.NEnd, - cfg.ExploitRunner.TeamIPFromN.OffsetX, - cfg.ExploitRunner.TeamIPFromN.OffsetY, - cfg.ExploitRunner.TeamIPFromN.Block, - cfg.ExploitRunner.TeamIPFromN.IPTemplate, - ) - err = s.addIps(ctx, ips, existTeams) - if err != nil { - log.Warn("error adding ips", slog.Any("ips", ips), prettylogger.Err(err)) + if cfg.ExploitRunner.TeamIPFromN != nil { + // add ip from N + ips := utils.ExpandIpFromN( + cfg.ExploitRunner.TeamIPFromN.NStart, + cfg.ExploitRunner.TeamIPFromN.NEnd, + cfg.ExploitRunner.TeamIPFromN.OffsetX, + cfg.ExploitRunner.TeamIPFromN.OffsetY, + cfg.ExploitRunner.TeamIPFromN.Block, + cfg.ExploitRunner.TeamIPFromN.IPTemplate, + ) + err = s.addIps(ctx, ips, existTeams) + if err != nil { + log.Warn("error adding ips", slog.Any("ips", ips), prettylogger.Err(err)) + } } if len(existTeams) > 0 {