Skip to content

Commit

Permalink
feat: add cli for scaffolding a new challenge
Browse files Browse the repository at this point in the history
create the class file and asciidoc file

Refs: #555
  • Loading branch information
Nanne Baars committed May 5, 2023
1 parent 5ad18a7 commit 26bd277
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 41 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ js/node/
js/node_modules/
node_modules
.npm
/cli/Cargo.lock
3 changes: 3 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ edition = "2021"

[dependencies]
clap = { version = "4.2.5", features = ["derive"] }
walkdir = "2"
regex = "1.8.1"
handlebars = "3"
12 changes: 10 additions & 2 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,16 @@ cargo build
target/debug/challenge-cli
```

## Running in IntelliJ

On `main.rs` right click and select `Run 'main'`. This will run the CLI in the terminal window of IntelliJ.
When passing command line arguments you need to add them to the run configuration. In IntelliJ go to `Run` -> `Edit Configurations...` and add the arguments to the `Command` field. You need to add `--` before the arguments. For example:

```shell
run --package challenge-cli --bin challenge-cli -- challenge -d easy -t git ../
```

## Todo

- Add option to pass in the project directory
- Create the directory structure for a new challenge
- Fix templating (not everything is present yet)
- Add GitHub actions to build binary for the different platforms
80 changes: 80 additions & 0 deletions cli/src/challenge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

use handlebars::Handlebars;
use walkdir::WalkDir;

use crate::{Difficulty, Platform, Technology};

pub struct Challenge {
pub number: u8,
pub technology: Technology,
pub difficulty: Difficulty,
pub project_directory: PathBuf,
pub platform: Platform,
}

impl std::fmt::Display for Challenge {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Technology: {}, Difficulty: {}", self.technology, self.difficulty)
}
}

pub fn create_challenge(challenge: &Challenge) {
let challenge_number = &challenge.number.to_string();
let challenge_name = String::from("Challenge") + challenge_number + ".java";
let challenge_exists = check_challenge_exists(challenge);

if challenge_exists {
panic!("{:?} already exists", challenge_name);
}

println!("Creating challenge {} in {}", challenge_number, challenge.project_directory.display());
create_challenge_class_file(challenge, challenge_number, challenge_name);
create_documentation_files(challenge, challenge_number);
}

fn create_documentation_files(challenge: &Challenge, challenge_number: &String) {
let challenge_documentation_path = challenge.project_directory.join("src/main/resources/explanations/");
create_documentation_file(challenge_documentation_path.join(format!("challenge{}.adoc", challenge_number)));
create_documentation_file(challenge_documentation_path.join(format!("challenge{}_hint.adoc", challenge_number)));
create_documentation_file(challenge_documentation_path.join(format!("challenge{}_explanation.adoc", challenge_number)));
}

fn create_documentation_file(filename: PathBuf) {
File::create(filename).expect("Unable to create challenge documentation file");
}

fn create_challenge_class_file(challenge: &Challenge, challenge_number: &String, challenge_name: String) {
const CHALLENGE_TEMPLATE: &str = "src/main/resources/challenge.hbs";
let challenge_source_path = challenge.project_directory.join("src/main/java/org/owasp/wrongsecrets/challenges");

let mut handlebars = Handlebars::new();
handlebars.register_template_file("challenge", challenge.project_directory.join(CHALLENGE_TEMPLATE)).unwrap();
let mut data = BTreeMap::new();
data.insert("challenge_number".to_string(), challenge_number);
let challenge_source_content = handlebars.render("challenge", &data).expect("Unable to render challenge template");
let mut class_file = File::create(challenge_source_path.join(challenge.platform.to_string()).join(challenge_name)).expect("Unable to create challenge source file");
class_file.write(challenge_source_content.as_bytes()).expect("Unable to write challenge source file");
}

//File API has `create_new` but it is still experimental in the nightly build, let loop and check if it exists for now
fn check_challenge_exists(challenge: &Challenge) -> bool {
let challenges_directory = challenge.project_directory.join("src/main/java/org/owasp/wrongsecrets/challenges");
let challenge_name = String::from("Challenge") + &challenge.number.to_string() + ".java";

let challenge_exists = WalkDir::new(challenges_directory)
.into_iter()
.filter_map(|e| e.ok())
.any(|e| {
match e.file_name().to_str() {
None => { false }
Some(name) => {
name.eq(challenge_name.as_str())
}
}
});
challenge_exists
}
15 changes: 14 additions & 1 deletion cli/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub enum Technology {
Documentation,
}

#[derive(clap::ValueEnum, Clone, Debug)]
#[derive(clap::ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum Difficulty {
Easy,
Normal,
Expand All @@ -36,6 +36,13 @@ pub enum Difficulty {
Master,
}

#[derive(clap::ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum Platform {
Cloud,
Docker,
Kubernetes
}

impl fmt::Display for Difficulty {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
Expand All @@ -47,3 +54,9 @@ impl fmt::Display for Technology {
write!(f, "{:?}", self)
}
}

impl fmt::Display for Platform {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}
106 changes: 68 additions & 38 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,48 +1,78 @@
use clap::{arg, Command};
use std::path::PathBuf;

use crate::enums::{Difficulty, Technology};
use clap::arg;
use clap::{Parser, Subcommand};

use crate::challenge::Challenge;
use crate::enums::{Difficulty, Platform, Technology};

mod enums;
mod challenge;

fn cli() -> Command {
Command::new("cli")
.about("A CLI for WrongSecrets")
.subcommand_required(true)
.arg_required_else_help(true)
.allow_external_subcommands(true)
.subcommand(
Command::new("challenge")
.about("Create a new challenge")
.arg_required_else_help(true)
.arg(
arg!(--"difficulty" <DIFFICULTY>)
.short('d')
.num_args(0..=1)
.value_parser(clap::builder::EnumValueParser::<Difficulty>::new())
.num_args(0..=1)
.default_value("easy")
)
.arg(
arg!(--"technology" <TECHNOLOGY>)
.short('t')
.value_parser(clap::builder::EnumValueParser::<Technology>::new())
.num_args(0..=1)
.require_equals(true)
.default_value("git")
)
)
#[derive(Debug, Parser)]
#[command(name = "cli")]
#[command(about = "A CLI for WrongSecrets", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}

fn main() {
let matches = cli().get_matches();
#[derive(Debug, Subcommand)]
enum Commands {
#[command(arg_required_else_help = true, name = "challenge", about = "Create a new challenge")]
ChallengeCommand {
//We could infer this from the directory structure but another PR could already have added the challenge with this number
#[arg(
long,
short,
value_name = "NUMBER")]
number: u8,
#[arg(
long,
short,
value_name = "DIFFICULTY",
num_args = 0..=1,
default_value_t = Difficulty::Easy,
default_missing_value = "easy",
value_enum
)]
difficulty: Difficulty,
#[arg(
long,
short,
value_name = "TECHNOLOGY",
num_args = 0..=1,
default_value_t = Technology::Git,
default_missing_value = "git",
value_enum
)]
technology: Technology,
#[arg(
long,
short,
value_name = "PLATFORM",
num_args = 0..=1,
value_enum
)]
platform: Platform,
#[arg(required = true)]
project_directory: PathBuf,
}
}

match matches.subcommand() {
Some(("challenge", sub_matches)) => {
println!(
"Create new challenge with difficulty: {}",
sub_matches.get_one::<Difficulty>("difficulty").expect("")
);
fn main() {
let args = Cli::parse();
match args.command {
Commands::ChallengeCommand {
number,
difficulty,
technology,
platform,
project_directory
} => {
project_directory.try_exists().expect("Unable to find project directory");
let challenge = Challenge { number, difficulty, technology, platform, project_directory };
challenge::create_challenge(&challenge);
}
_ => unreachable!()
}
}
75 changes: 75 additions & 0 deletions src/main/resources/challenge.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package org.owasp.wrongsecrets.challenges.{{platform}};

import org.owasp.wrongsecrets.RuntimeEnvironment;
import org.owasp.wrongsecrets.ScoreCard;
import org.owasp.wrongsecrets.challenges.Challenge;
import org.owasp.wrongsecrets.challenges.Difficulty;
import org.owasp.wrongsecrets.challenges.Spoiler;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.List;

{{
extra_imports
}}

@Component
@Order(0)
public class Challenge{{challenge_number}} extends Challenge {

public Challenge{{challenge_number}}(ScoreCard scoreCard) {
super(scoreCard);
}

/**
* {@inheritDoc}
*/
@Override
public Spoiler spoiler() {
return new Spoiler(getData());
}

/**
* {@inheritDoc}
*/
@Override
protected boolean answerCorrect(String answer) {
return getData().equals(answer);
}

@Override
/**
* {@inheritDoc}
*/
public List<RuntimeEnvironment.Environment> supportedRuntimeEnvironments() {
return List.of(DOCKER);
}

/**
* {@inheritDoc}
*/
@Override
public int difficulty() {
return Difficulty.{{difficulty}};
}

@Override
public String getTech() {
return ChallengeTechnology.Tech{{technology}}.id;
}

@Override
public boolean isLimitedWhenOnlineHosted() {
return false;
}

@Override
public boolean canRunInCTFMode() {
return true;
}

private String getData() {
return "<<replace with correct answer>>";
}
}

0 comments on commit 26bd277

Please sign in to comment.