Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add cli for scaffolding a new challenge #802

Closed
wants to merge 6 commits into from
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ js/node/
js/node_modules/
node_modules
.npm
/cli/Cargo.lock

# Cypress
cypress/videos
Expand Down
9 changes: 9 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you please add this path to the dependabot configuration :)

name = "challenge-cli"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.2.5", features = ["derive"] }
walkdir = "2"
handlebars = "3"
50 changes: 50 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# CLI for WrongSecrets

## Introduction

At the moment the CLI only serves one purpose: creating a new challenge. In the future more options can be added.

## Usage

```shell
./challenge-cli
```

will print:

```shell
A CLI for WrongSecrets

Usage: challenge-cli <COMMAND>

Commands:
challenge Create a new challenge
help Print this message or the help of the given subcommand(s)

Options:
-h, --help Print help
```

## Building

First install [Rust](https://www.rust-lang.org/tools/install). Then open a terminal and type:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe we can have a cross compiling and building github action with a download link to its attachments so people can copy it as well?


```shell
cd cli
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

- Fix templating (not everything is present yet)
Copy link
Collaborator

Choose a reason for hiding this comment

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

it would be nice to have some basic tests as well i guess :) ? Can you add those please?

- Add GitHub actions to build binary for the different platforms
Copy link
Collaborator

Choose a reason for hiding this comment

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

ah yes :D this!

110 changes: 110 additions & 0 deletions cli/src/challenge.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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};

#[derive(Debug)]
pub struct Challenge {
pub number: u8,
pub technology: Technology,
pub difficulty: Difficulty,
pub platform: Platform,
}

impl Challenge {
fn create_java_sources(&self, project_directory: &PathBuf) {
let challenge_source_path =
project_directory
.join("src/main/java/org/owasp/wrongsecrets/challenges");

let (handlebars, data) = self.init_handlebars(project_directory);
let challenge_source_content = handlebars
.render("challenge", &data)
.expect("Unable to render challenge template");
let mut class_file = File::create(
challenge_source_path
.join(self.platform.to_string())
.join(format!("Challenge{}.java", &self.number.to_string()))
)
.expect("Unable to create challenge source file");
class_file
.write(challenge_source_content.as_bytes())
.expect("Unable to write challenge source file");
}

fn init_handlebars(&self, project_directory: &PathBuf) -> (Handlebars, BTreeMap<String, String>) {
const CHALLENGE_TEMPLATE: &str = "src/main/resources/challenge.hbs";
let mut handlebars = Handlebars::new();
handlebars
.register_template_file(
"challenge",
project_directory.join(CHALLENGE_TEMPLATE),
)
.unwrap();
let mut data: BTreeMap<String, String> = BTreeMap::new();
data.insert("challenge_number".to_string(), self.number.to_string());
data.insert("platform".to_string(), self.platform.to_string().to_lowercase());
data.insert("difficulty".to_string(), self.difficulty.to_string().to_uppercase());
data.insert("technology".to_string(), self.technology.to_string().to_uppercase());

(handlebars, data)
}

fn create_documentation(&self, project_directory: &PathBuf) {
let challenge_documentation_path =
project_directory
.join("src/main/resources/explanations/");
create_documentation_file(
challenge_documentation_path.join(format!("challenge{}.adoc", self.number.to_string())),
);
create_documentation_file(
challenge_documentation_path.join(format!("challenge{}_hint.adoc", self.number.to_string())),
);
create_documentation_file(
challenge_documentation_path
.join(format!("challenge{}_explanation.adoc", self.number.to_string())),
);
}
}

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

pub fn create_challenge(challenge: &Challenge, project_directory: &PathBuf) {
let challenge_exists = check_challenge_exists(challenge, &project_directory);

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

println!(
"Creating {:?}",
&challenge
);
challenge.create_java_sources(project_directory);
challenge.create_documentation(project_directory);
}


//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, project_directory: &PathBuf) -> bool {
let challenges_directory =
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
}
62 changes: 62 additions & 0 deletions cli/src/enums.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Later on we can read this from the Github repository to make it more flexible
// cache the values locally and add a flag `--force` to force reading the values again
// Other option is to include a text file attached in a zip file. This makes it a bit more
// error prone as we need to have that file in the same directory.
// Other option is to have these files as part of the source code of wrongsecrets as you need
// to pass the project folder anyway. Otherwise generating a new challenge makes no sense ;-)

use std::fmt;

#[derive(clap::ValueEnum, Clone, Debug)]
pub enum Technology {
Git,
Docker,
ConfigMaps,
Secrets,
Vault,
Logging,
Terraform,
CSI,
CICD,
PasswordManager,
Cryptography,
Binary,
Frontend,
IAM,
Web3,
Documentation,
}

#[derive(clap::ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum Difficulty {
Easy,
Normal,
Hard,
Expert,
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)
}
}

impl fmt::Display for Technology {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}

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

use clap::{Parser, Subcommand};
use clap::arg;

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

mod challenge;
mod enums;

#[derive(Debug, Parser)]
#[command(name = "cli")]
#[command(about = "A CLI for WrongSecrets", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}

#[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,
},
}

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,
};
challenge::create_challenge(&challenge, &project_directory);
}
}
}
Loading