Skip to content

feat: add support for ssh challenge type along with docs#416

Open
DarkPhoenix42 wants to merge 1 commit intobl4ze/devfrom
feat/ssh-chall-type
Open

feat: add support for ssh challenge type along with docs#416
DarkPhoenix42 wants to merge 1 commit intobl4ze/devfrom
feat/ssh-chall-type

Conversation

@DarkPhoenix42
Copy link
Copy Markdown

No description provided.

Comment thread core/config/challenge.go Outdated
Comment thread core/config/challenge.go Outdated
Comment thread core/config/challenge.go
}
}

if !checkIfPortExistInMapping(portMappings, core.SSH_PORT) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the port mapping being appended GetPortMappings could be handled here.

@sukhman-sukh
Copy link
Copy Markdown
Collaborator

LGTM

@v1bh475u v1bh475u changed the base branch from chall-type-separation to bl4ze/dev January 21, 2026 11:58
Copilot AI review requested due to automatic review settings March 6, 2026 08:42
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Beast’s challenge deployment capabilities by adding an ssh challenge type and introducing Docker Compose-based deployment support, along with updated docs and new example challenges.

Changes:

  • Add ssh as a supported challenge type, including config validation updates.
  • Introduce Docker Compose staging/build/deploy/undeploy/cleanup flows (local + remote) and standardize project labeling.
  • Update documentation and add sample challenges for SSH and Docker Compose.

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
utils/id.go Adds GetProjectName helper for consistent Docker/Compose project naming.
pkg/remoteManager/file.go Adds remote Docker Compose image build support and applies build labels for non-compose builds.
pkg/remoteManager/container.go Adds remote Docker Compose deploy/down/purge helpers and service validation.
pkg/cr/images.go Adds local Docker Compose build support and adds build labels for standard docker builds.
pkg/cr/containers.go Adds container labels and local Docker Compose deploy/down/purge helpers.
docs/SampleChallenges.md Adds SSH challenge documentation and example configuration.
docs/ChallTypes.md Fixes headings and documents the new SSH challenge type.
core/utils/show.go Improves error returns and tag comparison logic.
core/utils/logs.go Removes stray whitespace in logging utils.
core/utils/cleanup.go Adds Docker Compose cleanup pathway and enables label as a cleanup filter.
core/manager/utils.go Persists DeploymentType in DB entries based on compose usage.
core/manager/pipeline.go Adds Docker Compose staging/commit/deploy flow and updates DB metadata handling.
core/manager/challenge.go Updates deploy/undeploy logic to handle Docker Compose deployments.
core/database/user.go Fixes transaction-start error formatting.
core/database/transactions.go Fixes transaction-start error formatting.
core/database/notification.go Fixes transaction-start error formatting.
core/database/challenges.go Adds DeploymentType field to Challenge model.
core/constants.go Adds ssh type constant, adds deployment type constants, updates SSH_PORT type, expands allowed challenge types.
core/config/challenge.go Adds docker_compose env field, SSH-specific port-mapping behavior, and compose validation/warnings.
_examples/simple-ssh/beast.toml Adds an SSH challenge example config.
_examples/simple-ssh/Dockerfile Adds an SSH challenge example container.
_examples/compose-type/docker-compose.yml Adds a multi-service Compose example.
_examples/compose-type/challenge/index.php Adds example web app for the Compose example.
_examples/compose-type/beast.toml Adds Compose-based challenge example config.
_examples/compose-type/README.md Adds instructions for deploying the Compose example.
_examples/compose-type/Dockerfile Adds Dockerfile for Compose app service.
_examples/README.md Updates examples index to include Docker Compose example and correct docker-type link.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread core/manager/pipeline.go
if err != nil {
return err
}
additionalCtx["docker-compose.yml"] = dockerCompose
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stageChallenge always embeds the compose file into the tar as docker-compose.yml, but later deployChallenge/Compose helpers use config.Challenge.Env.DockerCompose to build the -f <file> path. If the user specifies any other filename (e.g. compose.yml), the staged tar will contain docker-compose.yml while deployment will look for compose.yml and fail. Make the staged filename consistent with what deployment/build uses (e.g., preserve filepath.Base(config.Challenge.Env.DockerCompose) and pass -f everywhere, or normalize the config value to docker-compose.yml).

Suggested change
additionalCtx["docker-compose.yml"] = dockerCompose
// Normalize the staged compose filename so that later deployment/build
// steps, which rely on config.Challenge.Env.DockerCompose, use the same
// name as the one embedded into the tar/context.
config.Challenge.Env.DockerCompose = "docker-compose.yml"
additionalCtx[config.Challenge.Env.DockerCompose] = dockerCompose

Copilot uses AI. Check for mistakes.
Comment thread core/manager/pipeline.go
PortMapping: portMapping,
MountsMap: staticMount,
ImageId: challenge.ImageId,
ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name),
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cr.CreateContainerConfig still has a ChallengeName field (used to build the container name and now to set labels), but it is no longer populated here. This will result in empty/incorrect container names and labels (e.g. beast__abc) for standard docker deployments. Set ChallengeName: config.Challenge.Metadata.Name when constructing containerConfig.

Suggested change
ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name),
ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name),
ChallengeName: config.Challenge.Metadata.Name,

Copilot uses AI. Check for mistakes.
Comment thread docs/ChallTypes.md
```toml
# Docker file name for specific type challenge - `docker`.
# Helps to build flexible images for specific user-custom challenges
docket_context = ""
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Docker challenge config snippet the key is shown as docket_context, but the actual config field is docker_context. This typo will mislead users and cause config validation failures if copy/pasted.

Suggested change
docket_context = ""
docker_context = ""

Copilot uses AI. Check for mistakes.
primaryContainerId, err := getPrimaryComposeContainerIdRemote(projectName, server)
if err != nil {
log.Warnf("Could not get primary container ID for challenge %s on remote: %v", challengeName, err)
return "", nil // Return empty string but success
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both locally and remotely, if fetching the "primary" compose container ID fails, these helpers log a warning and return an empty container ID with a nil error. That empty ID can then get persisted to the DB (and other code assumes a non-empty / sliceable ContainerId), leading to confusing downstream failures. Consider returning a non-nil error instead, or return a defined sentinel (e.g. temp container id) without treating it as success.

Suggested change
return "", nil // Return empty string but success
return "", fmt.Errorf("could not get primary container ID for challenge %s on remote: %w", challengeName, err)

Copilot uses AI. Check for mistakes.
Comment thread core/manager/pipeline.go
Comment on lines +169 to +172
logBytes, buildErr = remoteManager.BuildImagesFromComposeRemote(
challengeName,
challengeTag,
stagedPath,
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the remote Docker Compose commit path, BuildImagesFromComposeRemote is invoked with stagedPath, which is the local tar path (e.g. $HOME/.beast/staging/...). On the remote host, the staged tar lives under core.BEAST_REMOTE_GLOBAL_DIR (as with the non-compose remote build path using remoteStagedPath). This will make remote compose builds fail because the tar file path won't exist on the server. Compute and pass the remote tar path (similar to the remoteStagedPath logic in the non-compose branch).

Suggested change
logBytes, buildErr = remoteManager.BuildImagesFromComposeRemote(
challengeName,
challengeTag,
stagedPath,
remoteStagedPath := filepath.Join(core.BEAST_REMOTE_GLOBAL_DIR, filepath.Base(stagedPath))
logBytes, buildErr = remoteManager.BuildImagesFromComposeRemote(
challengeName,
challengeTag,
remoteStagedPath,

Copilot uses AI. Check for mistakes.
Comment thread docs/SampleChallenges.md

Participants will be able to connect to this challenge using SSH:
```
ssh ctf-user@challenge-host -p 2222
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SSH example maps host port 14442 to container 22, but the connection snippet uses -p 2222. Update the SSH command to use the mapped host port (14442) or change the example mapping to match 2222 so readers can copy/paste successfully.

Suggested change
ssh ctf-user@challenge-host -p 2222
ssh ctf-user@challenge-host -p 14442

Copilot uses AI. Check for mistakes.
Comment thread pkg/cr/images.go
Comment on lines +118 to +139
func BuildImagesFromCompose(challengeName, challengeTag, stagedPath, ComposeFile string, noCache bool) (*bytes.Buffer, error) {
extractPath := filepath.Join(core.BEAST_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName, challengeName)

extractCmd := fmt.Sprintf("mkdir -p %s && tar -xf %s -C %s", extractPath, stagedPath, extractPath)
err := exec.Command("bash", "-c", extractCmd).Run()
if err != nil {
return nil, fmt.Errorf("error while extracting tar file %s to %s: %v", stagedPath, extractPath, err)
}

cmdArgs := []string{"compose", "build"}
if noCache {
cmdArgs = append(cmdArgs, "--no-cache")
}
// Note: docker compose build does not support --label flag
// Labels are automatically added to containers during 'docker compose up -p <project>'
composeCmd := fmt.Sprintf("docker %s", strings.Join(cmdArgs, " "))
log.Debugf("Building image for challenge %s with tag %s", challengeName, challengeTag)
log.Debugf("Running the command: docker %v", cmdArgs)

cmd := exec.Command("bash", "-c", composeCmd)
cmd.Dir = extractPath
var outBuffer bytes.Buffer
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildImagesFromCompose accepts a ComposeFile argument but currently ignores it and always runs docker compose build without -f. Combined with staging renaming behavior, this makes non-default compose filenames brittle. Also, using bash -c for both extraction and the docker command makes it harder to safely handle paths with spaces and increases shell-injection risk. Prefer using os.MkdirAll + exec.Command("tar", ... ) and exec.Command("docker", append([]string{"compose", "-f", <file>, "build"}, ... )...) (no shell).

Copilot uses AI. Check for mistakes.
Comment thread pkg/remoteManager/file.go
Comment on lines +112 to +125
func BuildImagesFromComposeRemote(challengeName, imageTag, stagedDir string, server config.AvailableServer, noCache bool) ([]byte, error) {
remoteExtractPath := filepath.Join(core.BEAST_REMOTE_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName, challengeName)
_, err := RunCommandOnServer(server, fmt.Sprintf("mkdir -p %s && tar -xf %s -C %s", remoteExtractPath, stagedDir, remoteExtractPath))
if err != nil {
return []byte{}, fmt.Errorf("failed to extract tar: %s", err)
}
cmdBase := "docker compose build"
if noCache {
cmdBase += " --no-cache"
}
// Note: docker compose build does not support --label flag
// Labels are automatically added to containers during 'docker compose up -p <project>'
dockerComposeBuildCmd := fmt.Sprintf("cd %s && %s", remoteExtractPath, cmdBase)

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BuildImagesFromComposeRemote (and the preceding tar extraction) builds a shell command string with unquoted, user-controlled paths (challengeName affects directories; stagedDir is a path) and executes it remotely. This is vulnerable to breaking on spaces and potentially command injection if a challenge directory/name contains shell metacharacters. Prefer quoting/escaping arguments or using a remote command runner that supports argv-style execution (or at least sh -c with safely shell-escaped values).

Copilot uses AI. Check for mistakes.
Comment thread pkg/cr/containers.go
primaryContainerId, err := getPrimaryComposeContainerId(projectName)
if err != nil {
log.Warnf("Could not get primary container ID for challenge %s: %v", challengeName, err)
return "", nil // Return empty string but success
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both locally and remotely, if fetching the "primary" compose container ID fails, these helpers log a warning and return an empty container ID with a nil error. That empty ID can then get persisted to the DB (and other code assumes a non-empty / sliceable ContainerId), leading to confusing downstream failures. Consider returning a non-nil error instead, or return a defined sentinel (e.g. temp container id) without treating it as success.

Suggested change
return "", nil // Return empty string but success
return "", fmt.Errorf("failed to get primary container ID for challenge %s: %w", challengeName, err)

Copilot uses AI. Check for mistakes.
Comment thread core/manager/pipeline.go

if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" {
server := cfg.Cfg.AvailableServers[challenge.ServerDeployed]
primaryContainerId, err = remoteManager.DeployContainerFromComposeRemote(challengeName, stagingDir, composeFileName, server)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the remote Docker Compose deploy path, stagingDir is constructed using core.BEAST_GLOBAL_DIR (a local filesystem path) and passed into DeployContainerFromComposeRemote, which then uses it to construct the compose file path on the remote server. For remote deployments this should use core.BEAST_REMOTE_GLOBAL_DIR (or another remote-resident path), otherwise docker compose -f <path> will point to a non-existent file on the server.

Suggested change
primaryContainerId, err = remoteManager.DeployContainerFromComposeRemote(challengeName, stagingDir, composeFileName, server)
remoteStagingDir := filepath.Join(core.BEAST_REMOTE_GLOBAL_DIR, core.BEAST_STAGING_DIR, challengeName)
primaryContainerId, err = remoteManager.DeployContainerFromComposeRemote(challengeName, remoteStagingDir, composeFileName, server)

Copilot uses AI. Check for mistakes.
Signed-off-by: Praneeth Sarode <praneethsarode@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants