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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Use Python 2.7 as the base image
FROM python:2.7

WORKDIR /app

COPY requirements.txt /app/

RUN pip install --no-cache-dir -r requirements.txt

COPY ./docs/ /app/docs/
COPY mkdocs.yml /app/
EXPOSE 8000

CMD ["mkdocs", "serve", "-a", "0.0.0.0:8000"]
3 changes: 0 additions & 3 deletions _examples/.static.beast.htpasswd

This file was deleted.

125 changes: 91 additions & 34 deletions _examples/example.config.toml
Original file line number Diff line number Diff line change
@@ -1,41 +1,98 @@
# $ ~/.beast
# ❮❮ tree
# .
# ├── auth_file
# ├── beast.db
# ├── config.toml
# └── .static.beast.htpasswd
# ├── remote/
# │   └── hack-test
# ├── secrets/
# │   ├── key.priv
# │   └── key.pub
# └── staging/
#
# Sample snapshot of .beast directory.
# Make sure the hack-test is a valid git directory
# Make sure you provide a valid existing authorized_keys_file
# Make sure key.pub is added to the deployed keys of the beast remote repo

# scripts_dir = "/home/vsts/.beast/scripts"
available_sidecars = ["mysql"]
jwt_secret = "beast"
allowed_base_images = ["ubuntu:16.04", "php:7.1-cli"]
authorized_keys_file = "/home/vsts/.beast/authorized_keys"
remote_sync_period = "0h2m0s"
# Authorized key file used by ssh daemon running on the host
# This is used for forwarding ssh connection to docker containers, the
# access to a container is only given to the author of the challenge.
authorized_keys_file = "$HOME/.beast/beast_authorized_keys"

[[remote]]
ssh_key = "/home/vsts/.beast/secret.key"
url = "[email protected]:sdslabs/nonexistent.git"
name = "nonexistent"
branch = "nonexistent"
# Directory which will contain all the autogenerated scripts by beast
# These scripts are the heart to above authorized keys file. Each entry in authorized
# keys file as a corresponding script which is executed during an SSH attempt.
scripts_dir = "$HOME/.beast/scripts"

# Base OS image that beast allows the challenges to use.
allowed_base_images = ["ubuntu:18.04", "ubuntu:16.04", "debian:jessie"]


# For authentication purposes beast uses JWT based authentication, this is the
# key used for encrypting the claims of a user. Keep this strong.
jwt_secret = "beast_jwt_secret_SUPER_STRONG_0x100010000100"


# To allow beast to send notification to a notification channel povide this webhook URL
# We are also working on implmeneting notification using IRC.
[[notification_webhooks]]

# The webhook URL of notification channel where notification should be sent
url = ""

# The service name to be used. It can be `discord` and `slack`
service_name = "discord"
active = true

[[notification_webhooks]]
url = ""
service_name = "slack"
# Status of webhook URL to be used.
# If it is false then notification will not be sent on this URL
active = false

# The sidecar that we support with beast, currently we only support two MySQL and
# MongoDB.
available_sidecars = ["mysql", "mongodb"]


# The frequency for any periodic event in beast, the value is provided in seconds.
# This is currently only used for health check periodic duration.s
ticker_frequency = 3000


# Container default resource limits for each challenge, this can be
# Overridden by challenge configuration beast.toml file.
default_cpu_shares = 1024
default_memory_limit = 1024
default_pids_limit = 100


# Configuration corresponding to the remote repository used by beast
# We use ssh authentication mechanism for interacting with git repository.
[[remote]]

# URL of the remote git repository, this should be user@host:<git_repository> format
url = "[email protected]:sdslabs/hack-test.git"

# Name of the remote
name = "hack-test"

# Branch we are tracking the remote in beast.
branch = "master"

# Path to private SSH key for interacting with the git repository.
ssh_key = "$HOME/.beast/secrets/key.priv"

# Status of remote git repository URL to be used
# If it is set to false then that remote git repository will not be used
active = false

# The following fields are required only while hosting a competition on beast
# This section contains information about the competition to be hosted
# Structure of the sections with the acceptable fields are:
[[competition_info]]
# Required Fields

# Name of the competition
name = ""

# About the competition
about = ""

# Starting time of competition wrt time zone in `16:31:23 UTC: +05:30, 17th February 2021, Wednesday` format
starting_time = ""

# Ending time of competition wrt time zone in `16:31:23 UTC: +05:30, 17th February 2021, Wednesday` format
ending_time = ""

# Time zone for reference in `Asia/Calcutta: UTC +05:30` format
timezone = ""

# Optional fields

# Prizes for the competition winners
prizes = ""

# Absolute path of logo file. Default logo dir is in the "BEAST_GLOBAL_DIR/assets/"
logo_url = ""
215 changes: 213 additions & 2 deletions api/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -458,9 +459,9 @@ func manageUploadHandler(c *gin.Context) {

// Extract and show from zip and return response
tempStageDir, err := manager.UnzipChallengeFolder(zipContextPath, core.BEAST_TEMP_DIR)

// log.Debug("The dir is ",tempStageDir)

// The file cannot be successfully un-zipped or the resultant was a malformed directory
if err != nil {
c.JSON(http.StatusBadRequest, HTTPErrorResp{
Expand Down Expand Up @@ -514,3 +515,213 @@ func manageUploadHandler(c *gin.Context) {
Points: config.Challenge.Metadata.Points,
})
}

// Create challenge configuration file
// @Summary Create a challenge configuration file from the fields provided
// @Description Handles the creation of a challenge configuration file from the fields provided in the form along with the files uploaded.
// @Tags manage
// @Accept json
// @Produce json
// @Param ChallengeConfig
// @Success 200 {object} api.HTTPPlainResp
// @Failure 400 {object} json
// @Router /api/manage/challenge/configure [post]
func manageConifgureHandler(c *gin.Context) {
var config cfg.BeastChallengeConfig

// Populating Author struct
config.Author.Name = c.PostForm("author_name")
config.Author.Email = c.PostForm("author_email")
config.Author.SSHKey = c.PostForm("author_ssh_key")
// Validate Author struct
err := config.Author.ValidateRequiredFields()
if err != nil {
utils.SendError(c, err)
return
}

// Populating Challenge Metadata struct
config.Challenge.Metadata.Name = c.PostForm("challenge_name")
config.Challenge.Metadata.Type = c.PostForm("challenge_type")
config.Challenge.Metadata.Flag = c.PostForm("challenge_flag")
config.Challenge.Metadata.Sidecar = c.PostForm("challenge_sidecar")
config.Challenge.Metadata.Tags = strings.Split(c.PostForm("challenge_tags"), ",")
config.Challenge.Metadata.Assets = strings.Split(c.PostForm("challenge_assets"), ",")
config.Challenge.Metadata.AdditionalLinks = strings.Split(c.PostForm("challenge_additional_links"), ",")
config.Challenge.Metadata.Hints = strings.Split(c.PostForm("challenge_hints"), ",")
config.Challenge.Metadata.Description = c.PostForm("challenge_desc")
maxpointsStr := c.PostForm("challenge_maxpoints")
maxpoints, err := strconv.ParseInt(maxpointsStr, 10, 64)
err = utils.ValidatePoints(int(maxpoints), err)
if err != nil {
utils.SendError(c, err)
return
}
config.Challenge.Metadata.MaxPoints = uint(maxpoints)
minpointsStr := c.PostForm("challenge_minpoints")
minpoints, err := strconv.ParseInt(minpointsStr, 10, 64)
err = utils.ValidatePoints(int(minpoints), err)
if err != nil {
utils.SendError(c, err)
return
}
config.Challenge.Metadata.MinPoints = uint(minpoints)
// Validate Challenge Metadata struct
err, _ = config.Challenge.Metadata.ValidateRequiredFields()
if err != nil {
utils.SendError(c, err)
return
}

// Populating Challenge Env
config.Challenge.Env.AptDeps = strings.Split(c.PostForm("challenge_apt_deps"), ",")
portsStr := c.PostForm("challenge_ports")
ports := strings.Split(portsStr, ",")
config.Challenge.Env.Ports = make([]uint32, len(ports))
for i, port := range ports {
portInt, err := strconv.ParseUint(port, 10, 32)
if err != nil {
err = fmt.Errorf("Port is not a valid port in %s: %s", portsStr, err)
utils.SendError(c, err)
return
}
config.Challenge.Env.Ports[i] = uint32(portInt)
}
if err = utils.CreateIfNotExistDir(core.BEAST_TEMP_DIR); err != nil {
if err := os.MkdirAll(core.BEAST_TEMP_DIR, 0755); err != nil {
err = fmt.Errorf("Could not create dir %s: %s", core.BEAST_TEMP_DIR, err)
utils.SendError(c, err)
return
}
}

// Preparing challenge directory
challroot := filepath.Join(core.BEAST_TEMP_DIR, config.Challenge.Metadata.Name)
err = utils.CreateIfNotExistDir(challroot)
if err != nil {
err = fmt.Errorf("Could not create dir %s: %s", config.Challenge.Metadata.Name, err)
utils.SendError(c, err)
}

files := c.Request.MultipartForm.File["challenge_setup_scripts"]
for _, fileHeader := range files {
config.Challenge.Env.SetupScripts = append(config.Challenge.Env.SetupScripts, fileHeader.Filename)
if err != nil {
utils.SendError(c, err)
return
}
err = c.SaveUploadedFile(fileHeader, challroot)
if utils.FileSaveError(c, err) != nil {
return
}
}
defaultport, err := strconv.ParseUint(c.PostForm("challenge_default_port"), 10, 32)
if err != nil {
err = fmt.Errorf("Port is not a valid port in %s: %s", portsStr, err)
utils.SendError(c, err)
return
}
config.Challenge.Env.DefaultPort = uint32(defaultport)
config.Challenge.Env.PortMappings = c.PostFormArray("challenge_port_mappings")

// Preparing public directory
publicdir := filepath.Join(challroot, core.PUBLIC)
err = utils.CreateIfNotExistDir(publicdir)
if err != nil {
err = fmt.Errorf("Could not create dir %s: %s", core.PUBLIC, err)
utils.SendError(c, err)
return
}
// if yes, we unzip the public.zip file given else we simply store it in the public directory
publicopt := c.PostForm("public_zip_btn")
publiczip := utils.FileDownload(c, "public_zip", publicdir)
zipContextPath := filepath.Join(publicdir, publiczip)
if publicopt == "yes" {
_, err = manager.UnzipChallengeFolder(zipContextPath, publicdir)
if err != nil {
err = fmt.Errorf("The unzip process failed or the ZIP was unacceptable: %s", err)
utils.SendError(c, err)
return
}
}
config.Challenge.Env.StaticContentDir = core.PUBLIC

// Preparing chall directory
challdir := filepath.Join(challroot, core.BEAST_CHALLENGE_DIR)
challzip := utils.FileDownload(c, "challenge_zip", challdir)
zipContextPath = filepath.Join(challdir, challzip)
_, err = manager.UnzipChallengeFolder(zipContextPath, challdir)
if err != nil {
err = fmt.Errorf("The unzip process failed or the ZIP was unacceptable: %s", err)
utils.SendError(c, err)
return
}
config.Challenge.Env.RunCmd = c.PostForm("challenge_run_cmd")
config.Challenge.Env.BaseImage = c.PostForm("challenge_base_image")
config.Challenge.Env.Entrypoint = c.PostForm("challenge_entrypoint")
config.Challenge.Env.WebRoot = c.PostForm("challenge_web_root")
config.Challenge.Env.ServicePath = c.PostForm("challenge_service_path")
config.Challenge.Env.DockerCtx = utils.FileDownload(c, "challenge_dockerfile", challroot)
config.Challenge.Env.Traffic = c.PostForm("challenge_traffic")
envVars := c.PostFormMap("challenge_env_vars")
for key, value := range envVars {
var EnvVar = cfg.EnvironmentVar{Key: key, Value: value}
config.Challenge.Env.EnvironmentVars = append(config.Challenge.Env.EnvironmentVars, EnvVar)
}
err = config.Challenge.Env.ValidateRequiredFields(config.Challenge.Metadata.Type, challroot)
if err != nil {
utils.SendError(c, err)
return
}

//Populating Resources
nCPU, err := strconv.ParseInt(c.PostForm("resources_cpu"), 10, 64)
if err != nil || nCPU < 0 {
err = fmt.Errorf("CPU is not a valid number: %s", err)
utils.SendError(c, err)
return
}
config.Resources.CPUShares = nCPU
mem, err := strconv.ParseInt(c.PostForm("resources_memory"), 10, 64)
if err != nil || mem < 0 {
err = fmt.Errorf("Memory is not a valid number: %s", err)
utils.SendError(c, err)
return
}
config.Resources.Memory = mem
pidslimit, err := strconv.ParseInt(c.PostForm("resources_pidslimit"), 10, 64)
if err != nil || pidslimit < 0 {
err = fmt.Errorf("Pids limit is not a valid number: %s", err)
utils.SendError(c, err)
return
}
config.Resources.PidsLimit = pidslimit
config.Resources.ValidateRequiredFields()
nMaintainers, err := strconv.ParseInt(c.PostForm("maintainers_count"), 10, 64)
if err != nil || nMaintainers < 0 {
err = fmt.Errorf("Maintainers count is not a valid number: %s", err)
utils.SendError(c, err)
return
}
for i := 0; i < int(nMaintainers); i++ {
config.Maintainers = append(config.Maintainers, cfg.Author{
Name: c.PostForm(fmt.Sprintf("maintainer_name_%d", i)),
Email: c.PostForm(fmt.Sprintf("maintainer_email_%d", i)),
SSHKey: c.PostForm(fmt.Sprintf("maintainer_ssh_key_%d", i)),
})
}
tomlfile := filepath.Join(challroot, core.CHALLENGE_CONFIG_FILE_NAME)
err = utils.WriteChallConfigFile(tomlfile, config)
if err != nil {
utils.SendError(c, err)
return
}
err = manager.CopyDir(challroot, filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_UPLOADS_DIR, config.Challenge.Metadata.Name))
if err != nil {
utils.SendError(c, err)
return
}
c.JSON(http.StatusOK, HTTPPlainResp{
Message: "Challenge configuration file created",
})
}
1 change: 1 addition & 0 deletions api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func initGinRouter() *gin.Engine {
manageGroup.POST("/challenge/verify", verifyHandler)
manageGroup.POST("/schedule/:action", manageScheduledAction)
manageGroup.POST("/challenge/upload", manageUploadHandler)
manageGroup.POST("/challenge/configure", manageConifgureHandler)
}

// Status route group
Expand Down
Loading