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 @@
-
@@ -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

-- **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
-
\ No newline at end of file
+
\ 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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
*-g~INo}78l-2H(w
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