feat: add support for ssh challenge type along with docs#416
feat: add support for ssh challenge type along with docs#416DarkPhoenix42 wants to merge 1 commit intobl4ze/devfrom
Conversation
| } | ||
| } | ||
|
|
||
| if !checkIfPortExistInMapping(portMappings, core.SSH_PORT) { |
There was a problem hiding this comment.
Maybe the port mapping being appended GetPortMappings could be handled here.
65e64ed to
09712d4
Compare
|
LGTM |
09712d4 to
05b2ec2
Compare
05b2ec2 to
a2132e3
Compare
There was a problem hiding this comment.
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
sshas 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.
| if err != nil { | ||
| return err | ||
| } | ||
| additionalCtx["docker-compose.yml"] = dockerCompose |
There was a problem hiding this comment.
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).
| 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 |
| PortMapping: portMapping, | ||
| MountsMap: staticMount, | ||
| ImageId: challenge.ImageId, | ||
| ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name), |
There was a problem hiding this comment.
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.
| ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name), | |
| ContainerName: coreUtils.EncodeID(config.Challenge.Metadata.Name), | |
| ChallengeName: config.Challenge.Metadata.Name, |
| ```toml | ||
| # Docker file name for specific type challenge - `docker`. | ||
| # Helps to build flexible images for specific user-custom challenges | ||
| docket_context = "" |
There was a problem hiding this comment.
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.
| docket_context = "" | |
| docker_context = "" |
| 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 |
There was a problem hiding this comment.
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.
| return "", nil // Return empty string but success | |
| return "", fmt.Errorf("could not get primary container ID for challenge %s on remote: %w", challengeName, err) |
| logBytes, buildErr = remoteManager.BuildImagesFromComposeRemote( | ||
| challengeName, | ||
| challengeTag, | ||
| stagedPath, |
There was a problem hiding this comment.
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).
| 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, |
|
|
||
| Participants will be able to connect to this challenge using SSH: | ||
| ``` | ||
| ssh ctf-user@challenge-host -p 2222 |
There was a problem hiding this comment.
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.
| ssh ctf-user@challenge-host -p 2222 | |
| ssh ctf-user@challenge-host -p 14442 |
| 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 |
There was a problem hiding this comment.
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).
| 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) | ||
|
|
There was a problem hiding this comment.
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).
| 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 |
There was a problem hiding this comment.
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.
| return "", nil // Return empty string but success | |
| return "", fmt.Errorf("failed to get primary container ID for challenge %s: %w", challengeName, err) |
|
|
||
| if challenge.ServerDeployed != core.LOCALHOST && challenge.ServerDeployed != "" { | ||
| server := cfg.Cfg.AvailableServers[challenge.ServerDeployed] | ||
| primaryContainerId, err = remoteManager.DeployContainerFromComposeRemote(challengeName, stagingDir, composeFileName, server) |
There was a problem hiding this comment.
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.
| 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) |
Signed-off-by: Praneeth Sarode <praneethsarode@gmail.com>
a2132e3 to
027c0a4
Compare
No description provided.