From e688869cfd82c3f87f34a54915a8ffa670e89d12 Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sat, 13 Dec 2025 21:27:59 +0700 Subject: [PATCH 1/9] add endpoint for python cli client --- jacfarm-api/internal/http/dto/service.go | 10 +++++++ .../internal/http/handlers/handlers.go | 1 + .../http/handlers/mocks/service_mock.go | 14 ++++++++++ jacfarm-api/internal/http/handlers/service.go | 27 +++++++++++++++++++ jacfarm-api/internal/http/server/router.go | 1 + jacfarm-api/internal/service/jacfarm/flags.go | 21 +++++++++++++++ 6 files changed, 74 insertions(+) create mode 100644 jacfarm-api/internal/http/dto/service.go create mode 100644 jacfarm-api/internal/http/handlers/service.go 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/router.go b/jacfarm-api/internal/http/server/router.go index a5bab66..fef2552 100644 --- a/jacfarm-api/internal/http/server/router.go +++ b/jacfarm-api/internal/http/server/router.go @@ -75,6 +75,7 @@ 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()) return r } diff --git a/jacfarm-api/internal/service/jacfarm/flags.go b/jacfarm-api/internal/service/jacfarm/flags.go index 9ad25a1..52e46c4 100644 --- a/jacfarm-api/internal/service/jacfarm/flags.go +++ b/jacfarm-api/internal/service/jacfarm/flags.go @@ -45,6 +45,27 @@ 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", prettylogger.Err(err)) + return err + } + } + log.Info("flags send successfully", slog.Int("count", len(req.Flags))) + + return nil +} + func (s *Service) GetFlagsCount(ctx context.Context) (int, error) { const op = "service.jacfarm.GetFlagsCount" log := s.log.With(slog.String("op", op)) From b7a8230df66ac85f3009eaa86b53af06cb23ff03 Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sat, 13 Dec 2025 22:30:05 +0700 Subject: [PATCH 2/9] add cli tool for starting exploits --- cli/cmd/cli/main.go | 38 ++++++++++ cli/go.mod | 12 ++++ cli/go.sum | 13 ++++ cli/internal/cli/cli.go | 78 +++++++++++++++++++++ cli/internal/clients/jacfarm/client.go | 66 ++++++++++++++++++ cli/internal/worker/worker.go | 97 ++++++++++++++++++++++++++ 6 files changed, 304 insertions(+) create mode 100644 cli/cmd/cli/main.go create mode 100644 cli/go.mod create mode 100644 cli/go.sum create mode 100644 cli/internal/cli/cli.go create mode 100644 cli/internal/clients/jacfarm/client.go create mode 100644 cli/internal/worker/worker.go diff --git a/cli/cmd/cli/main.go b/cli/cmd/cli/main.go new file mode 100644 index 0000000..1eae018 --- /dev/null +++ b/cli/cmd/cli/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "cli_exploit_runner/internal/cli" + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + + "github.com/jacute/prettylogger" +) + +func main() { + args, err := cli.ParseArgs() + if err != nil { + fmt.Printf("usage: %s \n-help for more information\n", os.Args[0]) + os.Exit(1) + } + + 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 { + fmt.Println(err.Error()) + 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..b52bdb8 --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,12 @@ +module cli_exploit_runner + +go 1.25.4 + +require github.com/jacute/prettylogger v0.0.7 + +require ( + 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 + golang.org/x/sys v0.18.0 // indirect +) diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 0000000..1233dda --- /dev/null +++ b/cli/go.sum @@ -0,0 +1,13 @@ +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= +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= diff --git a/cli/internal/cli/cli.go b/cli/internal/cli/cli.go new file mode 100644 index 0000000..3c53f1a --- /dev/null +++ b/cli/internal/cli/cli.go @@ -0,0 +1,78 @@ +package cli + +import ( + jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" + "cli_exploit_runner/internal/worker" + "errors" + "flag" + "fmt" + "log/slog" + "time" + + "github.com/jacute/prettylogger" +) + +var ( + ErrUsage = errors.New("usage error") +) + +type Args struct { + Addr 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.IntVar(&args.AttackPeriod, "a", 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.Parse() + + rest := flag.Args() + if len(rest) < 1 { + return nil, ErrUsage + } + + args.Addr = rest[0] + args.ExecutablePath = rest[1] + + return &args, 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, + 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 %e", op, err) + } + w, err := worker.New( + client, log, + 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) + } + + go func() { + 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..ab18924 --- /dev/null +++ b/cli/internal/clients/jacfarm/client.go @@ -0,0 +1,66 @@ +package jacfarm_client + +import ( + "fmt" + "net/http" + "time" +) + +const defaultPort = 15050 +const defaultTimeout = 5 * time.Second + +type Client struct { + addr string + httpClient *http.Client +} + +type options struct { + port *int + timeout time.Duration // int seconds +} + +type Option func(opts *options) error + +func New(host 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 + } + + return &Client{ + addr: fmt.Sprintf("%s:%d", host, port), + httpClient: &http.Client{ + Timeout: timeout, + }, + }, 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 + } +} diff --git a/cli/internal/worker/worker.go b/cli/internal/worker/worker.go new file mode 100644 index 0000000..df815f1 --- /dev/null +++ b/cli/internal/worker/worker.go @@ -0,0 +1,97 @@ +package worker + +import ( + "log/slog" + "time" +) + +const ( + defaultAttackPeriod = 5 * time.Second + defaultMaxConcurrentExploits = 5 +) + +type JacFARMClient interface { +} + +type Worker struct { + client JacFARMClient + attackPeriod time.Duration + maxConcurrentExploits int + + log *slog.Logger + stopCh chan struct{} +} + +type options struct { + attackPeriod *time.Duration + maxConcurrentExploits *int +} + +type Option func(opts *options) error + +func New(client JacFARMClient, log *slog.Logger, opts ...Option) (*Worker, error) { + workerOpts := &options{} + for _, opt := range opts { + err := opt(workerOpts) + if err != nil { + return nil, err + } + } + + w := &Worker{ + client: client, + attackPeriod: defaultAttackPeriod, + maxConcurrentExploits: defaultMaxConcurrentExploits, + + log: log, + stopCh: make(chan struct{}), + } + + 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 + } +} + +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), + ) + for { + select { + case <-w.stopCh: + return + default: + + } + } +} + +func (w *Worker) Stop() { + const op = "worker.Stop" + w.log.Info("stopping worker") + w.stopCh <- struct{}{} +} From 23c414f7376f6f40a3dea2580e7ee5480ddbeade Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sat, 17 Jan 2026 13:49:10 +0700 Subject: [PATCH 3/9] cli: update worker & client --- cli/cmd/cli/main.go | 5 + cli/internal/cli/cli.go | 19 ++- cli/internal/clients/jacfarm/client.go | 134 ++++++++++++++++- cli/internal/clients/jacfarm/dto.go | 41 +++++ cli/internal/worker/executor.go | 48 ++++++ cli/internal/worker/sender.go | 41 +++++ cli/internal/worker/worker.go | 141 ++++++++++++++++-- cli/pkg/common_config/config.go | 16 ++ .../internal/http/server/config_test.go | 8 +- .../internal/http/server/exploits_test.go | 16 +- jacfarm-api/internal/http/server/router.go | 1 + jacfarm-api/internal/service/jacfarm/flags.go | 3 +- 12 files changed, 447 insertions(+), 26 deletions(-) create mode 100644 cli/internal/clients/jacfarm/dto.go create mode 100644 cli/internal/worker/executor.go create mode 100644 cli/internal/worker/sender.go create mode 100644 cli/pkg/common_config/config.go diff --git a/cli/cmd/cli/main.go b/cli/cmd/cli/main.go index 1eae018..2e2c606 100644 --- a/cli/cmd/cli/main.go +++ b/cli/cmd/cli/main.go @@ -17,6 +17,11 @@ func main() { fmt.Printf("usage: %s \n-help for more information\n", os.Args[0]) os.Exit(1) } + err = cli.ValidateArgs(args) + if err != nil { + fmt.Println(err.Error()) + os.Exit(2) + } log := slog.New(prettylogger.NewColoredHandler(os.Stdout, &slog.HandlerOptions{ Level: slog.LevelInfo, diff --git a/cli/internal/cli/cli.go b/cli/internal/cli/cli.go index 3c53f1a..1f115e0 100644 --- a/cli/internal/cli/cli.go +++ b/cli/internal/cli/cli.go @@ -7,6 +7,7 @@ import ( "flag" "fmt" "log/slog" + "os" "time" "github.com/jacute/prettylogger" @@ -18,6 +19,8 @@ var ( type Args struct { Addr string + FlagRe string + Token string Port int ExecutablePath string Timeout int @@ -29,9 +32,11 @@ 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, "a", 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.Parse() @@ -46,6 +51,13 @@ func ParseArgs() (*Args, error) { return &args, nil } +func ValidateArgs(args *Args) error { + if _, err := os.Stat(args.ExecutablePath); err != nil { + return err + } + return nil +} + // Run starts the worker // Non-block function func Run(args *Args, log *slog.Logger) error { @@ -53,6 +65,7 @@ func Run(args *Args, log *slog.Logger) error { client, err := jacfarm_client.New( args.Addr, + args.Token, jacfarm_client.WithCustomPort(args.Port), jacfarm_client.WithTimeout(time.Duration(args.Timeout)*time.Second), ) @@ -62,6 +75,8 @@ func Run(args *Args, log *slog.Logger) error { } w, err := worker.New( client, log, + args.ExecutablePath, + args.FlagRe, worker.WithAttackPeriod(time.Duration(args.AttackPeriod)*time.Second), worker.WithMaxConcurrentExploits(args.MaxConcurrentExploits), ) @@ -70,9 +85,7 @@ func Run(args *Args, log *slog.Logger) error { return fmt.Errorf("%s: error creating worker %e", op, err) } - go func() { - w.Run() - }() + w.Run() return nil } diff --git a/cli/internal/clients/jacfarm/client.go b/cli/internal/clients/jacfarm/client.go index ab18924..04cdf84 100644 --- a/cli/internal/clients/jacfarm/client.go +++ b/cli/internal/clients/jacfarm/client.go @@ -1,17 +1,28 @@ package jacfarm_client import ( + "bytes" + "cli_exploit_runner/pkg/common_config" + "context" + "encoding/json" + "errors" "fmt" + "io" "net/http" "time" ) +var ( + ErrFlagFormatNotFound = errors.New("flag format not found") +) + const defaultPort = 15050 const defaultTimeout = 5 * time.Second type Client struct { addr string httpClient *http.Client + token string } type options struct { @@ -21,7 +32,7 @@ type options struct { type Option func(opts *options) error -func New(host string, opts ...Option) (*Client, error) { +func New(host string, token string, opts ...Option) (*Client, error) { options := &options{} for _, opt := range opts { err := opt(options) @@ -42,12 +53,23 @@ func New(host string, opts ...Option) (*Client, error) { timeout = options.timeout } - return &Client{ + c := &Client{ addr: fmt.Sprintf("%s:%d", host, port), httpClient: &http.Client{ Timeout: timeout, }, - }, nil + token: token, + } + + // trying to ping + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _, err := c.GetTeams(ctx) + if err != nil { + return nil, err + } + + return c, nil } func WithTimeout(timeout time.Duration) Option { @@ -64,3 +86,109 @@ func WithCustomPort(port int) Option { return nil } } + +func (c *Client) GetTeams(ctx context.Context) ([]*Team, error) { + url := fmt.Sprintf("http://%s/api/v1/service/teams", c.addr) + 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 teams []*Team + if err := json.Unmarshal(data, &teams); err != nil { + return nil, err + } + + return teams, nil +} + +func (c *Client) getConfig(ctx context.Context) ([]*Config, error) { + url := fmt.Sprintf("http://%s/api/v1/service/config", c.addr) + 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("http://%s/api/v1/service/flags", c.addr) + req, err := http.NewRequestWithContext(ctx, "PUT", url, nil) + if err != nil { + return err + } + req.Header.Set("Authorization", c.token) + req.Header.Set("Content-Type", "application/json") + + data, err := json.Marshal(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..1d95845 --- /dev/null +++ b/cli/internal/worker/executor.go @@ -0,0 +1,48 @@ +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) + + 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/sender.go b/cli/internal/worker/sender.go new file mode 100644 index 0000000..5f0433d --- /dev/null +++ b/cli/internal/worker/sender.go @@ -0,0 +1,41 @@ +package worker + +import ( + jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" + "context" + "log/slog" + + "github.com/jacute/prettylogger" +) + +const senderSize = 50 + +func (w *Worker) runSender() { + const op = "worker.startSender" + log := w.log.With(slog.String("op", op)) + + flagBuffer := make([]*jacfarm_client.ServiceFlag, senderSize) + + for { + select { + case <-w.stopCh: + for _, flag := range <-w.flagQueue { + flagBuffer = append(flagBuffer, flag) + } + err := w.client.SendFlags(context.Background(), flagBuffer) + if err != nil { + log.Error("error sending flags", prettylogger.Err(err)) + } + return + 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)) + } + flagBuffer = flagBuffer[:0] + } + } + } +} diff --git a/cli/internal/worker/worker.go b/cli/internal/worker/worker.go index df815f1..d55680a 100644 --- a/cli/internal/worker/worker.go +++ b/cli/internal/worker/worker.go @@ -1,8 +1,24 @@ package worker import ( + jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" + "context" + "errors" + "fmt" "log/slog" + "net" + "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 ( @@ -11,15 +27,20 @@ const ( ) 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{} + log *slog.Logger + stopCh chan struct{} + flagQueue chan []*jacfarm_client.ServiceFlag } type options struct { @@ -29,7 +50,13 @@ type options struct { type Option func(opts *options) error -func New(client JacFARMClient, log *slog.Logger, opts ...Option) (*Worker, 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) @@ -38,13 +65,20 @@ func New(client JacFARMClient, log *slog.Logger, opts ...Option) (*Worker, error } } + flagRe, err := regexp.Compile(flagRegexp) + if err != nil { + return nil, err + } + w := &Worker{ client: client, attackPeriod: defaultAttackPeriod, maxConcurrentExploits: defaultMaxConcurrentExploits, + flagRe: flagRe, - log: log, - stopCh: make(chan struct{}), + log: log, + stopCh: make(chan struct{}), + flagQueue: make(chan []*jacfarm_client.ServiceFlag, 1), // TODO: optimization of buf size } if workerOpts.attackPeriod != nil { @@ -71,23 +105,106 @@ func WithMaxConcurrentExploits(count int) Option { } } +// 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), ) - for { - select { - case <-w.stopCh: - return - default: - } + // 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 == os.ErrNotExist { + return fmt.Errorf("exploit %s does not exist", w.exploitPath) + } + if err != nil { + 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 { + cmd := exec.CommandContext(ctx, w.exploitPath, string(t.IP)) + concurrentCh <- struct{}{} + go func() { + defer func() { + wg.Done() + <-concurrentCh + }() + out, err := attack(t.IP, cmd) + 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(ip net.IP, exploitProc *exec.Cmd) (exploitOut []byte, err error) { + if err = exploitProc.Start(); err != nil { + return nil, err + } + out, err := exploitProc.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() { 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/jacfarm-api/internal/http/server/config_test.go b/jacfarm-api/internal/http/server/config_test.go index 61a18bd..c0fafa5 100644 --- a/jacfarm-api/internal/http/server/config_test.go +++ b/jacfarm-api/internal/http/server/config_test.go @@ -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 fef2552..42a8b02 100644 --- a/jacfarm-api/internal/http/server/router.go +++ b/jacfarm-api/internal/http/server/router.go @@ -76,6 +76,7 @@ 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/service/jacfarm/flags.go b/jacfarm-api/internal/service/jacfarm/flags.go index 52e46c4..293f20e 100644 --- a/jacfarm-api/internal/service/jacfarm/flags.go +++ b/jacfarm-api/internal/service/jacfarm/flags.go @@ -57,11 +57,10 @@ func (s *Service) ServicePutFlag(ctx context.Context, req *dto.ServicePutFlagReq CreatedAt: time.Now().UTC(), }) if err != nil { - log.Error("error sending flags to queue", prettylogger.Err(err)) + log.Error("error sending flags to queue", slog.Any("flag", flag), prettylogger.Err(err)) return err } } - log.Info("flags send successfully", slog.Int("count", len(req.Flags))) return nil } From 30c0ba07c389376585c9af708762541f71356c0c Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sat, 17 Jan 2026 15:16:06 +0700 Subject: [PATCH 4/9] WIP: add tests to cli --- Makefile | 2 +- cli/cmd/cli/main.go | 5 +- cli/go.mod | 9 +- cli/go.sum | 12 ++ cli/internal/cli/cli.go | 20 ++- cli/internal/clients/jacfarm/client.go | 23 ++-- cli/internal/worker/mocks/matcher.go | 58 ++++++++ cli/internal/worker/mocks/worker_mock.go | 71 ++++++++++ cli/internal/worker/sender.go | 20 +-- cli/internal/worker/testcases/testsploit.sh | 6 + cli/internal/worker/worker.go | 22 ++- cli/internal/worker/worker_test.go | 127 ++++++++++++++++++ .../internal/http/server/config_test.go | 24 ++-- 13 files changed, 351 insertions(+), 48 deletions(-) create mode 100644 cli/internal/worker/mocks/matcher.go create mode 100644 cli/internal/worker/mocks/worker_mock.go create mode 100755 cli/internal/worker/testcases/testsploit.sh create mode 100644 cli/internal/worker/worker_test.go 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/cli/cmd/cli/main.go b/cli/cmd/cli/main.go index 2e2c606..9eea8b4 100644 --- a/cli/cmd/cli/main.go +++ b/cli/cmd/cli/main.go @@ -2,6 +2,7 @@ package main import ( "cli_exploit_runner/internal/cli" + "flag" "fmt" "log/slog" "os" @@ -14,12 +15,13 @@ import ( func main() { args, err := cli.ParseArgs() if err != nil { - fmt.Printf("usage: %s \n-help for more information\n", os.Args[0]) + flag.Usage() os.Exit(1) } err = cli.ValidateArgs(args) if err != nil { fmt.Println(err.Error()) + flag.Usage() os.Exit(2) } @@ -33,7 +35,6 @@ func main() { log.Info("running script") err = cli.Run(args, log) if err != nil { - fmt.Println(err.Error()) os.Exit(2) } diff --git a/cli/go.mod b/cli/go.mod index b52bdb8..27c2ea2 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -2,11 +2,18 @@ module cli_exploit_runner go 1.25.4 -require github.com/jacute/prettylogger v0.0.7 +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 index 1233dda..2bb0d07 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,3 +1,5 @@ +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= @@ -7,7 +9,17 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk 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 index 1f115e0..abc024f 100644 --- a/cli/internal/cli/cli.go +++ b/cli/internal/cli/cli.go @@ -33,16 +33,28 @@ func ParseArgs() (*Args, error) { flag.IntVar(&args.Timeout, "t", 5, "timeout for http client (in seconds)") flag.StringVar(&args.Token, "a", "", "JacFARM auth token") - flag.IntVar(&args.AttackPeriod, "a", 5, "attack period (in seconds)") + 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, ErrUsage + return nil, flag.ErrHelp } args.Addr = rest[0] @@ -52,7 +64,7 @@ func ParseArgs() (*Args, error) { } func ValidateArgs(args *Args) error { - if _, err := os.Stat(args.ExecutablePath); err != nil { + if _, err := os.Stat(args.ExecutablePath); os.IsNotExist(err) { return err } return nil @@ -71,7 +83,7 @@ func Run(args *Args, log *slog.Logger) error { ) if err != nil { log.Error("error creating jacfarm client", prettylogger.Err(err)) - return fmt.Errorf("%s: error creating jacfarm client %e", op, err) + return fmt.Errorf("%s: error creating jacfarm client %w", op, err) } w, err := worker.New( client, log, diff --git a/cli/internal/clients/jacfarm/client.go b/cli/internal/clients/jacfarm/client.go index 04cdf84..e6f8e59 100644 --- a/cli/internal/clients/jacfarm/client.go +++ b/cli/internal/clients/jacfarm/client.go @@ -14,13 +14,14 @@ import ( var ( ErrFlagFormatNotFound = errors.New("flag format not found") + ErrAuth = errors.New("auth error") ) const defaultPort = 15050 const defaultTimeout = 5 * time.Second type Client struct { - addr string + baseURL string httpClient *http.Client token string } @@ -54,7 +55,7 @@ func New(host string, token string, opts ...Option) (*Client, error) { } c := &Client{ - addr: fmt.Sprintf("%s:%d", host, port), + baseURL: fmt.Sprintf("http://%s:%d/jacfarm-api", host, port), httpClient: &http.Client{ Timeout: timeout, }, @@ -66,6 +67,9 @@ func New(host string, token string, opts ...Option) (*Client, error) { 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 } @@ -88,7 +92,7 @@ func WithCustomPort(port int) Option { } func (c *Client) GetTeams(ctx context.Context) ([]*Team, error) { - url := fmt.Sprintf("http://%s/api/v1/service/teams", c.addr) + url := fmt.Sprintf("%s/api/v1/service/teams", c.baseURL) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err @@ -101,6 +105,9 @@ func (c *Client) GetTeams(ctx context.Context) ([]*Team, error) { } 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) } @@ -110,16 +117,16 @@ func (c *Client) GetTeams(ctx context.Context) ([]*Team, error) { return nil, err } - var teams []*Team - if err := json.Unmarshal(data, &teams); err != nil { + var resBody *ListTeamsResponse + if err := json.Unmarshal(data, &resBody); err != nil { return nil, err } - return teams, nil + return resBody.Teams, nil } func (c *Client) getConfig(ctx context.Context) ([]*Config, error) { - url := fmt.Sprintf("http://%s/api/v1/service/config", c.addr) + url := fmt.Sprintf("%s/api/v1/service/config", c.baseURL) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err @@ -165,7 +172,7 @@ func (c *Client) GetFlagFormat(ctx context.Context) (string, error) { } func (c *Client) SendFlags(ctx context.Context, flags []*ServiceFlag) error { - url := fmt.Sprintf("http://%s/api/v1/service/flags", c.addr) + url := fmt.Sprintf("%s/api/v1/service/flags", c.baseURL) req, err := http.NewRequestWithContext(ctx, "PUT", url, nil) if err != nil { return err 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..7f83f08 --- /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 ( + jacfarm "cli_exploit_runner/internal/clients/jacfarm" + context "context" + 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 index 5f0433d..b3e9785 100644 --- a/cli/internal/worker/sender.go +++ b/cli/internal/worker/sender.go @@ -14,19 +14,23 @@ func (w *Worker) runSender() { const op = "worker.startSender" log := w.log.With(slog.String("op", op)) - flagBuffer := make([]*jacfarm_client.ServiceFlag, senderSize) + flagBuffer := make([]*jacfarm_client.ServiceFlag, 0, senderSize) for { select { case <-w.stopCh: - for _, flag := range <-w.flagQueue { - flagBuffer = append(flagBuffer, flag) - } - err := w.client.SendFlags(context.Background(), flagBuffer) - if err != nil { - log.Error("error sending flags", prettylogger.Err(err)) + for { + select { + case flags := <-w.flagQueue: + flagBuffer = append(flagBuffer, flags...) + default: + err := w.client.SendFlags(context.Background(), flagBuffer) + if err != nil { + log.Error("error sending flags", prettylogger.Err(err)) + } + return + } } - return case flags := <-w.flagQueue: flagBuffer = append(flagBuffer, flags...) if len(flagBuffer) >= senderSize { 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 index d55680a..4f4e8b4 100644 --- a/cli/internal/worker/worker.go +++ b/cli/internal/worker/worker.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "log/slog" - "net" "os" "os/exec" "regexp" @@ -26,6 +25,7 @@ const ( 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 @@ -74,11 +74,12 @@ func New( client: client, attackPeriod: defaultAttackPeriod, maxConcurrentExploits: defaultMaxConcurrentExploits, + exploitPath: exploitPath, flagRe: flagRe, log: log, stopCh: make(chan struct{}), - flagQueue: make(chan []*jacfarm_client.ServiceFlag, 1), // TODO: optimization of buf size + flagQueue: make(chan []*jacfarm_client.ServiceFlag, senderSize), // TODO: optimization of buf size } if workerOpts.attackPeriod != nil { @@ -133,10 +134,10 @@ func (w *Worker) attackAll( log := w.log.With(slog.String("op", op), slog.String("exploit_path", w.exploitPath)) info, err := os.Stat(w.exploitPath) - if err == os.ErrNotExist { - return fmt.Errorf("exploit %s does not exist", w.exploitPath) - } if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("exploit %s does not exist", w.exploitPath) + } return err } if info.IsDir() { @@ -153,14 +154,13 @@ func (w *Worker) attackAll( wg.Add(len(teams)) for _, t := range teams { - cmd := exec.CommandContext(ctx, w.exploitPath, string(t.IP)) concurrentCh <- struct{}{} go func() { defer func() { wg.Done() <-concurrentCh }() - out, err := attack(t.IP, cmd) + out, err := attack(ctx, w.exploitPath, t.IP.String()) if err != nil { log.Error( "error attacking team", @@ -186,11 +186,9 @@ func (w *Worker) attackAll( return nil } -func attack(ip net.IP, exploitProc *exec.Cmd) (exploitOut []byte, err error) { - if err = exploitProc.Start(); err != nil { - return nil, err - } - out, err := exploitProc.Output() +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 } diff --git a/cli/internal/worker/worker_test.go b/cli/internal/worker/worker_test.go new file mode 100644 index 0000000..e5664db --- /dev/null +++ b/cli/internal/worker/worker_test.go @@ -0,0 +1,127 @@ +package worker + +import ( + jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" + "cli_exploit_runner/internal/worker/mocks" + "context" + "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) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + testcases := []struct { + name string + flags []string + worker func() *Worker + }{ + { + name: "ok", + flags: []string{ + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", + }, + worker: func() *Worker { + 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(2) + 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(200*time.Millisecond), + ) + if err != nil { + t.Fatal(err) + } + + return w + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(tt *testing.T) { + w := tc.worker() + w.Run() + + time.Sleep(1 * time.Second) + w.Stop() + }) + } +} diff --git a/jacfarm-api/internal/http/server/config_test.go b/jacfarm-api/internal/http/server/config_test.go index c0fafa5..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 { From b7683cc52583a9d788752ff3c8c017db5d0266ae Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sun, 18 Jan 2026 15:27:32 +0700 Subject: [PATCH 5/9] fix: TestAttackAll --- cli/internal/worker/sender.go | 8 +++++--- cli/internal/worker/worker.go | 2 +- cli/internal/worker/worker_test.go | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cli/internal/worker/sender.go b/cli/internal/worker/sender.go index b3e9785..0eaf1c7 100644 --- a/cli/internal/worker/sender.go +++ b/cli/internal/worker/sender.go @@ -24,9 +24,11 @@ func (w *Worker) runSender() { case flags := <-w.flagQueue: flagBuffer = append(flagBuffer, flags...) default: - err := w.client.SendFlags(context.Background(), flagBuffer) - if err != nil { - log.Error("error sending flags", prettylogger.Err(err)) + if len(flagBuffer) > 0 { + err := w.client.SendFlags(context.Background(), flagBuffer) + if err != nil { + log.Error("error sending flags", prettylogger.Err(err)) + } } return } diff --git a/cli/internal/worker/worker.go b/cli/internal/worker/worker.go index 4f4e8b4..a6ffbdb 100644 --- a/cli/internal/worker/worker.go +++ b/cli/internal/worker/worker.go @@ -208,5 +208,5 @@ func parseFlags(text []byte, re *regexp.Regexp) []string { func (w *Worker) Stop() { const op = "worker.Stop" w.log.Info("stopping worker") - w.stopCh <- struct{}{} + close(w.stopCh) } diff --git a/cli/internal/worker/worker_test.go b/cli/internal/worker/worker_test.go index e5664db..265f4c6 100644 --- a/cli/internal/worker/worker_test.go +++ b/cli/internal/worker/worker_test.go @@ -39,12 +39,10 @@ func TestAttack(t *testing.T) { } func TestAttackAll(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() testcases := []struct { name string flags []string - worker func() *Worker + worker func() (*Worker, *gomock.Controller) }{ { name: "ok", @@ -59,7 +57,8 @@ func TestAttackAll(t *testing.T) { "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB=", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC=", }, - worker: func() *Worker { + worker: func() (*Worker, *gomock.Controller) { + ctrl := gomock.NewController(t) clientMock := mocks.NewMockJacFARMClient(ctrl) clientMock.EXPECT().GetTeams(gomock.Any()).Return([]*jacfarm_client.Team{ { @@ -72,7 +71,7 @@ func TestAttackAll(t *testing.T) { Name: "aboba2", IP: net.ParseIP("1.1.1.2"), }, - }, nil).Times(2) + }, nil).Times(1) clientMock.EXPECT().SendFlags(gomock.Any(), mocks.UnorderedSlice([]*jacfarm_client.ServiceFlag{ { Flag: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", @@ -104,24 +103,25 @@ func TestAttackAll(t *testing.T) { slog.New(prettylogger.NewDiscardHandler()), "testcases/testsploit.sh", "[A-Z0-9]{31}=", - WithAttackPeriod(200*time.Millisecond), + WithAttackPeriod(1*time.Second), ) if err != nil { t.Fatal(err) } - return w + return w, ctrl }, }, } for _, tc := range testcases { t.Run(tc.name, func(tt *testing.T) { - w := tc.worker() + w, ctrl := tc.worker() w.Run() - - time.Sleep(1 * time.Second) + time.Sleep(1500 * time.Millisecond) w.Stop() + time.Sleep(1 * time.Second) + ctrl.Finish() }) } } From fbb6ae9ba7ac4a1df61e7103b3ed30d85c6f1430 Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sun, 18 Jan 2026 16:01:50 +0700 Subject: [PATCH 6/9] update cli worker --- cli/internal/clients/jacfarm/client.go | 6 ++++-- cli/internal/worker/executor.go | 1 + cli/internal/worker/sender.go | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cli/internal/clients/jacfarm/client.go b/cli/internal/clients/jacfarm/client.go index e6f8e59..8706c2a 100644 --- a/cli/internal/clients/jacfarm/client.go +++ b/cli/internal/clients/jacfarm/client.go @@ -173,14 +173,16 @@ func (c *Client) GetFlagFormat(ctx context.Context) (string, error) { 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, "PUT", url, nil) + 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(flags) + data, err := json.Marshal(&ServicePutFlagRequest{ + Flags: flags, + }) if err != nil { return err } diff --git a/cli/internal/worker/executor.go b/cli/internal/worker/executor.go index 1d95845..22151a2 100644 --- a/cli/internal/worker/executor.go +++ b/cli/internal/worker/executor.go @@ -13,6 +13,7 @@ func (w *Worker) runExecutor() { log := w.log.With(slog.String("op", op)) timer := time.NewTimer(w.attackPeriod) + defer timer.Stop() for { select { diff --git a/cli/internal/worker/sender.go b/cli/internal/worker/sender.go index 0eaf1c7..04f6b8f 100644 --- a/cli/internal/worker/sender.go +++ b/cli/internal/worker/sender.go @@ -4,16 +4,20 @@ import ( jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" "context" "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 { @@ -28,17 +32,32 @@ func (w *Worker) runSender() { 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] } From 28d0e506818322eb241f9c415b0ff49c75894e37 Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sun, 18 Jan 2026 16:02:19 +0700 Subject: [PATCH 7/9] add dedup header --- jacfarm-api/internal/rabbitmq/flags_queue.go | 3 +++ jacfarm-api/internal/rabbitmq/rabbitmq.go | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 { From 469786dc3f96cdb0e3521df8cec9dc4ff5580121 Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sun, 18 Jan 2026 16:03:19 +0700 Subject: [PATCH 8/9] jacfarm-api: rename utils pkg to zip --- jacfarm-api/internal/service/jacfarm/exploits.go | 4 ++-- .../{utils => zip}/testcases/001_file195KB.zip | Bin .../testcases/002_total_size_390KB.zip | Bin .../{utils => zip}/testcases/003_path_traversal.zip | Bin .../{utils => zip}/testcases/004_double_dot.zip | Bin jacfarm-api/internal/{utils/utils.go => zip/zip.go} | 2 +- .../{utils/utils_test.go => zip/zip_test.go} | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) rename jacfarm-api/internal/{utils => zip}/testcases/001_file195KB.zip (100%) rename jacfarm-api/internal/{utils => zip}/testcases/002_total_size_390KB.zip (100%) rename jacfarm-api/internal/{utils => zip}/testcases/003_path_traversal.zip (100%) rename jacfarm-api/internal/{utils => zip}/testcases/004_double_dot.zip (100%) rename jacfarm-api/internal/{utils/utils.go => zip/zip.go} (99%) rename jacfarm-api/internal/{utils/utils_test.go => zip/zip_test.go} (99%) 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/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" From eb2df61c9dbf0cb84ee3059b456e99101079dad6 Mon Sep 17 00:00:00 2001 From: Egor Bochkarev Date: Sun, 18 Jan 2026 16:19:54 +0700 Subject: [PATCH 9/9] update README.md for cli --- README.md | 16 +- cli/cmd/cli/main.go | 2 +- cli/go.mod | 2 +- cli/internal/cli/cli.go | 4 +- cli/internal/clients/jacfarm/client.go | 2 +- cli/internal/worker/mocks/worker_mock.go | 2 +- cli/internal/worker/sender.go | 2 +- cli/internal/worker/worker.go | 2 +- cli/internal/worker/worker_test.go | 4 +- docs/diagram.drawio | 183 +++++++++--------- docs/img/diagram.jpg | Bin 79125 -> 0 bytes docs/img/diagram.png | Bin 0 -> 89886 bytes .../config_loader/internal/service/config.go | 26 +-- 13 files changed, 127 insertions(+), 118 deletions(-) delete mode 100644 docs/img/diagram.jpg create mode 100644 docs/img/diagram.png 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 index 9eea8b4..eeb5b8f 100644 --- a/cli/cmd/cli/main.go +++ b/cli/cmd/cli/main.go @@ -1,9 +1,9 @@ package main import ( - "cli_exploit_runner/internal/cli" "flag" "fmt" + "jacfarmcli/internal/cli" "log/slog" "os" "os/signal" diff --git a/cli/go.mod b/cli/go.mod index 27c2ea2..bc0e2e0 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -1,4 +1,4 @@ -module cli_exploit_runner +module jacfarmcli go 1.25.4 diff --git a/cli/internal/cli/cli.go b/cli/internal/cli/cli.go index abc024f..7c77d35 100644 --- a/cli/internal/cli/cli.go +++ b/cli/internal/cli/cli.go @@ -1,11 +1,11 @@ package cli import ( - jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" - "cli_exploit_runner/internal/worker" "errors" "flag" "fmt" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" + "jacfarmcli/internal/worker" "log/slog" "os" "time" diff --git a/cli/internal/clients/jacfarm/client.go b/cli/internal/clients/jacfarm/client.go index 8706c2a..2a5fff8 100644 --- a/cli/internal/clients/jacfarm/client.go +++ b/cli/internal/clients/jacfarm/client.go @@ -2,12 +2,12 @@ package jacfarm_client import ( "bytes" - "cli_exploit_runner/pkg/common_config" "context" "encoding/json" "errors" "fmt" "io" + "jacfarmcli/pkg/common_config" "net/http" "time" ) diff --git a/cli/internal/worker/mocks/worker_mock.go b/cli/internal/worker/mocks/worker_mock.go index 7f83f08..da93724 100644 --- a/cli/internal/worker/mocks/worker_mock.go +++ b/cli/internal/worker/mocks/worker_mock.go @@ -10,8 +10,8 @@ package mocks import ( - jacfarm "cli_exploit_runner/internal/clients/jacfarm" context "context" + jacfarm "jacfarmcli/internal/clients/jacfarm" reflect "reflect" gomock "go.uber.org/mock/gomock" diff --git a/cli/internal/worker/sender.go b/cli/internal/worker/sender.go index 04f6b8f..204202f 100644 --- a/cli/internal/worker/sender.go +++ b/cli/internal/worker/sender.go @@ -1,8 +1,8 @@ package worker import ( - jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" "context" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" "log/slog" "time" diff --git a/cli/internal/worker/worker.go b/cli/internal/worker/worker.go index a6ffbdb..e9ec0cc 100644 --- a/cli/internal/worker/worker.go +++ b/cli/internal/worker/worker.go @@ -1,10 +1,10 @@ package worker import ( - jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" "context" "errors" "fmt" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" "log/slog" "os" "os/exec" diff --git a/cli/internal/worker/worker_test.go b/cli/internal/worker/worker_test.go index 265f4c6..f54f2dc 100644 --- a/cli/internal/worker/worker_test.go +++ b/cli/internal/worker/worker_test.go @@ -1,9 +1,9 @@ package worker import ( - jacfarm_client "cli_exploit_runner/internal/clients/jacfarm" - "cli_exploit_runner/internal/worker/mocks" "context" + jacfarm_client "jacfarmcli/internal/clients/jacfarm" + "jacfarmcli/internal/worker/mocks" "log/slog" "net" "os" 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 fc361bd6cd1b0b04739e69a3b2b85d6d409e3846..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 79125 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 diff --git a/docs/img/diagram.png b/docs/img/diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3d6824b24b9704fbba038185d2f2d242becf85 GIT binary patch literal 89886 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 literal 0 HcmV?d00001 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 {