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