-
-
Notifications
You must be signed in to change notification settings - Fork 372
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
Changes from 3 commits
d42437a
5ad18a7
26bd277
28840b0
96180be
7fd3abf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,3 +72,4 @@ js/node/ | |
js/node_modules/ | ||
node_modules | ||
.npm | ||
/cli/Cargo.lock |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
[package] | ||
name = "challenge-cli" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
clap = { version = "4.2.5", features = ["derive"] } | ||
walkdir = "2" | ||
regex = "1.8.1" | ||
handlebars = "3" |
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yes :D this! |
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 | ||
} |
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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
use std::path::PathBuf; | ||
|
||
use clap::arg; | ||
use clap::{Parser, Subcommand}; | ||
|
||
use crate::challenge::Challenge; | ||
use crate::enums::{Difficulty, Platform, Technology}; | ||
|
||
mod enums; | ||
mod challenge; | ||
|
||
#[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, project_directory }; | ||
challenge::create_challenge(&challenge); | ||
} | ||
} | ||
} |
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 | ||||||||||||
}} | ||||||||||||
|
||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
@Component | ||||||||||||
@Order(0) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we make the order also automatede in terms o the number? |
||||||||||||
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}}; | ||||||||||||
} | ||||||||||||
|
||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
@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>>"; | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
} | ||||||||||||
} |
There was a problem hiding this comment.
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 :)