diff --git a/.gitignore b/.gitignore index 9ab6def..125f4ee 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ go.work.sum *.db .vscode + +CLAUDE.md +.claude \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e972e7..651602c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ We welcome contributions from everyone! Here's how you can contribute: 2. **Create a New Branch** - Make your changes - - If you want to add a new vulnerable target template, check [the example template](./templates/example-template/index.yaml) + - If you want to add a new vulnerable target template, please contribute to our separate templates repository: [vt-templates](https://github.com/HappyHackingSpace/vt-templates). You can check the [example template](https://github.com/HappyHackingSpace/vt-templates/blob/main/cves/vt-2024-53995/index.yaml). - Test your changes locally before submitting 3. **Submit a Pull Request** diff --git a/README.md b/README.md index 2ea6429..38ca300 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,18 @@ Vulnerable Target (VT) is a specialized tool designed for security professionals > This tool creates intentionally vulnerable environments. Running this on a public server or an insecure network can expose you to severe security risks. Use only in an isolated, local environment (sandbox/VM). ## Features -- Community-Curated List of Vulnerable Targets -- Interactive Vulnerability Playground (TODO) -- CLI (In-Progress) +- CLI for managing vulnerable environments +- Docker Compose provider for container orchestration +- Community-curated templates from [vt-templates](https://github.com/HappyHackingSpace/vt-templates) +- Template filtering by tags +- Deployment state tracking + +## Prerequisites +- Go 1.24+ +- Docker & Docker Compose + +## Installation -## Quickstart 1. Clone the repository ```bash git clone https://github.com/HappyHackingSpace/vulnerable-target.git @@ -28,10 +35,45 @@ cd vulnerable-target go mod download ``` -3. Run the application with: +3. Build the binary +```bash +go build -o vt cmd/vt/main.go +``` + +4. (Optional) Move to your PATH +```bash +mv vt /usr/local/bin/ +``` + +## Usage + ```bash -go run cmd/vt/main.go +# List available templates +vt template --list + +# Filter templates by tag +vt template --list --filter sql + +# Update templates from remote repository +vt template --update + +# Start a vulnerable environment +vt start --id --provider docker-compose + +# List running environments +vt ps + +# Stop an environment +vt stop --id --provider docker-compose + +# Set verbosity level +vt -v debug ``` + +## Templates + +Templates are automatically cloned to `~/vt-templates` on first run. To contribute new vulnerable target templates, visit the [vt-templates repository](https://github.com/HappyHackingSpace/vt-templates). + ## Documentation Check the full documentation here: [Vulnerable Target Wiki](https://github.com/HappyHackingSpace/vulnerable-target/wiki) diff --git a/cmd/vt/main.go b/cmd/vt/main.go index c019883..eb9f7c7 100644 --- a/cmd/vt/main.go +++ b/cmd/vt/main.go @@ -2,19 +2,40 @@ package main import ( - "github.com/happyhackingspace/vulnerable-target/internal/banner" + "github.com/happyhackingspace/vulnerable-target/internal/app" "github.com/happyhackingspace/vulnerable-target/internal/cli" "github.com/happyhackingspace/vulnerable-target/internal/logger" - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + "github.com/happyhackingspace/vulnerable-target/internal/state" + "github.com/happyhackingspace/vulnerable-target/pkg/provider/registry" + "github.com/happyhackingspace/vulnerable-target/pkg/store/disk" + "github.com/happyhackingspace/vulnerable-target/pkg/template" + "github.com/rs/zerolog/log" ) func main() { - // Initialize logger and templates explicitly - logger.Init() - templates.Init() + cfg := app.DefaultConfig() - banner.Print() + appLogger := logger.NewWithLevel(cfg.LogLevel) + logger.SetGlobal(appLogger) - // Run the CLI - cli.Run() + templates, err := template.LoadTemplates(cfg.TemplatesPath) + if err != nil { + log.Fatal().Err(err).Msg("failed to load templates") + } + + storeCfg := disk.NewConfig(). + WithFileName("deployments.db"). + WithBucketName("deployments") + stateManager, err := state.NewManager(storeCfg) + if err != nil { + log.Fatal().Err(err).Msg("failed to create state manager") + } + + providers := registry.NewProviders(stateManager) + + application := app.NewApp(templates, providers, stateManager, cfg) + + if err := cli.New(application).Run(); err != nil { + log.Fatal().Err(err).Msg("CLI error") + } } diff --git a/go.mod b/go.mod index 247a470..095539f 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/compose-spec/compose-go/v2 v2.8.1 github.com/docker/cli v28.3.3+incompatible github.com/docker/compose/v2 v2.39.2 + github.com/go-git/go-git/v5 v5.16.3 github.com/jedib0t/go-pretty/v6 v6.6.6 github.com/rs/zerolog v1.33.0 github.com/spf13/cobra v1.9.1 @@ -73,7 +74,6 @@ require ( github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect - github.com/go-git/go-git/v5 v5.16.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -114,7 +114,6 @@ require ( github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/miekg/pkcs11 v1.1.1 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/buildkit v0.23.0-rc1.0.20250618182037-9b91d20367db // indirect @@ -158,7 +157,6 @@ require ( github.com/skeema/knownhosts v1.3.1 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/spf13/pflag v1.0.7 // indirect - github.com/src-d/gcfg v1.4.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 // indirect github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 // indirect @@ -205,8 +203,6 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect - gopkg.in/src-d/go-git.v4 v4.13.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect k8s.io/api v0.32.3 // indirect k8s.io/apimachinery v0.32.3 // indirect diff --git a/go.sum b/go.sum index ea994ae..c858abe 100644 --- a/go.sum +++ b/go.sum @@ -25,12 +25,12 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d h1:hi6J4K6D github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -122,7 +122,6 @@ github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++ github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= @@ -168,15 +167,15 @@ github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNE github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 h1:XBBHcIb256gUJtLmY22n99HaZTz+r2Z51xUPi01m3wg= github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203/go.mod h1:E1jcSv8FaEny+OP/5k9UxZVw9YFWGj7eI4KR/iOBqCg= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -184,15 +183,16 @@ github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQ github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= -github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA= -github.com/go-git/go-git v4.7.0+incompatible/go.mod h1:6+421e08gnZWn30y26Vchf7efgYLe4dl5OQbBSUXShE= -github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= -github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.3 h1:Z8BtvxZ09bYm/yYNgPKCzgWtaRqDTgIKRgIRHBfU6Z8= +github.com/go-git/go-git/v5 v5.16.3/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -226,8 +226,6 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -285,7 +283,6 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.6.6 h1:LyezkL+1SuqH2z47e5IMQkYUIcs2BD+MnpdPRiRcN0c= github.com/jedib0t/go-pretty/v6 v6.6.6/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8 h1:CZkYfurY6KGhVtlalI4QwQ6T0Cu6iuY3e0x5RLu96WE= github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d h1:jRQLvyVGL+iVtDElaEIDdKwpPqUIZJfzkNLV34htpEc= @@ -303,7 +300,6 @@ github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVE github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -318,7 +314,6 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= @@ -350,8 +345,6 @@ github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyex github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -423,7 +416,6 @@ github.com/opencontainers/selinux v1.12.0 h1:6n5JV4Cf+4y0KNXW48TLj5DwfXpvWlxXplU github.com/opencontainers/selinux v1.12.0/go.mod h1:BTPX+bjVbWGXw7ZZWUbdENt8w0htPSrlgOOysQaU62U= github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= @@ -462,9 +454,8 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -473,7 +464,6 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+x github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/secure-systems-lab/go-securesystemslib v0.6.0 h1:T65atpAVCJQK14UA57LMdZGpHi4QYSH/9FZyNGqMYIA= github.com/secure-systems-lab/go-securesystemslib v0.6.0/go.mod h1:8Mtpo9JKks/qhPG4HGZ2LGMvrPbzuxwfz/f/zLfEWkk= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQEo87pStk/a99dzIO1mM9KxIyLPGTU= @@ -505,8 +495,6 @@ github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c h1:2EejZtjFjKJGk71ANb+wtFK5EjUzUkEM3R0xnp559xg= github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -541,7 +529,6 @@ github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnn github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -599,10 +586,8 @@ go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -621,7 +606,6 @@ golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -644,10 +628,8 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -674,7 +656,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -684,7 +665,6 @@ golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -709,7 +689,6 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/cenkalti/backoff.v2 v2.2.1 h1:eJ9UAg01/HIHG987TwxvnzK2MgxXq97YY6rYDpY9aII= gopkg.in/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -724,11 +703,6 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM= gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw= -gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= -gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..7cd5ae2 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,61 @@ +// Package app provides the application dependency container and configuration. +package app + +import ( + "os" + "path/filepath" + + "github.com/happyhackingspace/vulnerable-target/internal/state" + "github.com/happyhackingspace/vulnerable-target/pkg/provider" + "github.com/happyhackingspace/vulnerable-target/pkg/template" +) + +// Config holds application configuration. +type Config struct { + TemplatesPath string + StoragePath string + LogLevel string +} + +// App is the dependency container for the application. +type App struct { + Templates map[string]template.Template + Providers map[string]provider.Provider + StateManager *state.Manager + Config *Config +} + +// DefaultConfig returns the default application configuration. +func DefaultConfig() *Config { + homeDir, err := os.UserHomeDir() + if err != nil { + homeDir = "." + } + + return &Config{ + TemplatesPath: filepath.Join(homeDir, "vt-templates"), + StoragePath: filepath.Join(homeDir, ".vt-cli"), + LogLevel: "info", + } +} + +// NewApp creates a new App instance with the given dependencies. +func NewApp( + templates map[string]template.Template, + providers map[string]provider.Provider, + stateManager *state.Manager, + config *Config, +) *App { + return &App{ + Templates: templates, + Providers: providers, + StateManager: stateManager, + Config: config, + } +} + +// GetProvider retrieves a provider by name from the app's provider map. +func (a *App) GetProvider(name string) (provider.Provider, bool) { + p, ok := a.Providers[name] + return p, ok +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go new file mode 100644 index 0000000..fadffdb --- /dev/null +++ b/internal/cli/cli.go @@ -0,0 +1,80 @@ +// Package cli provides command-line interface functionality for the vulnerable target application. +package cli + +import ( + "fmt" + "maps" + "slices" + "strings" + + "github.com/happyhackingspace/vulnerable-target/internal/app" + "github.com/happyhackingspace/vulnerable-target/internal/banner" + "github.com/happyhackingspace/vulnerable-target/internal/logger" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// logLevels defines the valid log levels supported by the application. +var logLevels = map[string]bool{ + zerolog.DebugLevel.String(): true, + zerolog.InfoLevel.String(): true, + zerolog.WarnLevel.String(): true, + zerolog.ErrorLevel.String(): true, + zerolog.FatalLevel.String(): true, + zerolog.PanicLevel.String(): true, +} + +// CLI encapsulates the command-line interface with its dependencies. +type CLI struct { + app *app.App + rootCmd *cobra.Command +} + +// New creates a new CLI instance with the given application context. +func New(application *app.App) *CLI { + c := &CLI{ + app: application, + } + c.setupCommands() + return c +} + +// setupCommands initializes all CLI commands and their configurations. +func (c *CLI) setupCommands() { + c.rootCmd = &cobra.Command{ + Use: "vt", + Short: "Create vulnerable environment", + Version: banner.AppVersion, + PersistentPreRun: func(cmd *cobra.Command, _ []string) { + verbosityLevel, err := cmd.Flags().GetString("verbosity") + if err != nil { + log.Fatal().Msgf("%v", err) + } + logger.InitWithLevel(verbosityLevel) + }, + SilenceErrors: true, + } + c.rootCmd.SetHelpTemplate(banner.Banner() + "\n" + c.rootCmd.HelpTemplate()) + + // Setup root flags + c.rootCmd.PersistentFlags().StringP("verbosity", "v", zerolog.InfoLevel.String(), + fmt.Sprintf("Set the verbosity level for logs (%s)", + strings.Join(slices.Collect(maps.Keys(logLevels)), ", "))) + + // Register all subcommands + c.rootCmd.AddCommand(c.newStartCommand()) + c.rootCmd.AddCommand(c.newStopCommand()) + c.rootCmd.AddCommand(c.newPsCommand()) + c.rootCmd.AddCommand(c.newTemplateCommand()) +} + +// Run executes the CLI and returns any error. +func (c *CLI) Run() error { + return c.rootCmd.Execute() +} + +// providerNames returns a slice of registered provider names. +func (c *CLI) providerNames() []string { + return slices.Collect(maps.Keys(c.app.Providers)) +} diff --git a/internal/cli/list.go b/internal/cli/list.go index e22f501..5b48121 100644 --- a/internal/cli/list.go +++ b/internal/cli/list.go @@ -1,64 +1,79 @@ package cli import ( - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + tmpl "github.com/happyhackingspace/vulnerable-target/pkg/template" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -var templateCmd = &cobra.Command{ - Use: "template", - Short: "template operations", - Run: func(cmd *cobra.Command, _ []string) { - list, err := cmd.Flags().GetBool("list") - if err != nil { - log.Error().Err(err).Msg("failed to get list flag") - return - } +// newTemplateCommand creates the template command. +func (c *CLI) newTemplateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "template", + Short: "template operations", + Run: func(cmd *cobra.Command, _ []string) { + list, err := cmd.Flags().GetBool("list") + if err != nil { + log.Error().Err(err).Msg("failed to get list flag") + return + } - update, err := cmd.Flags().GetBool("update") - if err != nil { - log.Error().Err(err).Msg("failed to get update flag") - return - } + update, err := cmd.Flags().GetBool("update") + if err != nil { + log.Error().Err(err).Msg("failed to get update flag") + return + } - filter, err := cmd.Flags().GetString("filter") - if err != nil { - log.Error().Err(err).Msg("failed to get filter flag") - return - } + filter, err := cmd.Flags().GetString("filter") + if err != nil { + log.Error().Err(err).Msg("failed to get filter flag") + return + } - if list && update { - log.Error().Msg("only one of --list, or --update can be specified") - return - } + if list && update { + log.Error().Msg("only one of --list, or --update can be specified") + return + } - if filter != "" && !list { - log.Error().Msg("--filter can only be used with --list") - return - } + if filter != "" && !list { + log.Error().Msg("--filter can only be used with --list") + return + } - if !list && !update { - if err := cmd.Help(); err != nil { - log.Error().Err(err).Msg("failed to display help") + if !list && !update { + if err := cmd.Help(); err != nil { + log.Error().Err(err).Msg("failed to display help") + } + return } - return - } - if list { - templates.ListWithFilter(filter) - return - } + if list { + tmpl.ListTemplatesWithFilter(c.app.Templates, filter) + return + } - if update { - templates.SyncTemplates() - return - } - }, -} + if update { + if err := tmpl.SyncTemplates(c.app.Config.TemplatesPath); err != nil { + log.Error().Err(err).Msg("failed to sync templates") + return + } + // Reload templates after sync + templates, err := tmpl.LoadTemplates(c.app.Config.TemplatesPath) + if err != nil { + log.Error().Err(err).Msg("failed to reload templates") + return + } + // Update the app's templates + c.app.Templates = templates + log.Info().Msg("Templates updated successfully") + return + } + }, + } + + cmd.Flags().BoolP("list", "l", false, "List available templates") + cmd.Flags().BoolP("update", "u", false, "Fetch templates repository to local working directory") + cmd.Flags().StringP("filter", "f", "", "Filter templates by tag or keyword (only works with --list)") -func setupTemplateCommand() { - templateCmd.Flags().BoolP("list", "l", false, "List available templates") - templateCmd.Flags().BoolP("update", "u", false, "Fetch templates repository to local working directory") - templateCmd.Flags().StringP("filter", "f", "", "Filter templates by tag or keyword (only works with --list)") + return cmd } diff --git a/internal/cli/ps.go b/internal/cli/ps.go index 766233b..c509bb9 100644 --- a/internal/cli/ps.go +++ b/internal/cli/ps.go @@ -4,72 +4,64 @@ import ( "os" "time" - "github.com/happyhackingspace/vulnerable-target/internal/state" - "github.com/happyhackingspace/vulnerable-target/pkg/provider/registry" - "github.com/happyhackingspace/vulnerable-target/pkg/store/disk" - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + tmpl "github.com/happyhackingspace/vulnerable-target/pkg/template" "github.com/jedib0t/go-pretty/v6/table" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -var psCmd = &cobra.Command{ - Use: "ps", - Short: "List running deployments and their status", - Run: func(_ *cobra.Command, _ []string) { - cfg := disk.NewConfig().WithFileName("deployments.db").WithBucketName("deployments") - st, err := state.NewManager(cfg) - if err != nil { - log.Error().Msgf("%v", err) - return - } - - deployments, err := st.ListDeployments() - if err != nil { - log.Error().Msgf("%v", err) - return - } - - t := table.NewWriter() - t.SetStyle(table.StyleDefault) - t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"Provider Name", "Template ID", "Status", "Created At"}) - - count := 0 - for _, deployment := range deployments { - provider := registry.GetProvider(deployment.ProviderName) - if provider == nil { - log.Error().Msgf("provider %q not found", deployment.ProviderName) - continue - } - template, err := templates.GetByID(deployment.TemplateID) +// newPsCommand creates the ps command. +func (c *CLI) newPsCommand() *cobra.Command { + return &cobra.Command{ + Use: "ps", + Short: "List running deployments and their status", + Run: func(_ *cobra.Command, _ []string) { + deployments, err := c.app.StateManager.ListDeployments() if err != nil { log.Error().Msgf("%v", err) - continue + return } - status := "unknown" - if s, err := provider.Status(template); err != nil { - log.Error().Msgf("%v", err) - } else { - status = s - } + t := table.NewWriter() + t.SetStyle(table.StyleDefault) + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Provider Name", "Template ID", "Status", "Created At"}) + + count := 0 + for _, deployment := range deployments { + provider, ok := c.app.GetProvider(deployment.ProviderName) + if !ok { + log.Error().Msgf("provider %q not found", deployment.ProviderName) + continue + } + template, err := tmpl.GetByID(c.app.Templates, deployment.TemplateID) + if err != nil { + log.Error().Msgf("%v", err) + continue + } - t.AppendRow(table.Row{ - deployment.ProviderName, - deployment.TemplateID, - status, - deployment.CreatedAt.Format(time.DateTime), - }) - count++ - } + status := "unknown" + if s, err := provider.Status(template); err != nil { + log.Error().Msgf("%v", err) + } else { + status = s + } - if count == 0 { - log.Info().Msg("there is no running environment") - return - } + t.AppendRow(table.Row{ + deployment.ProviderName, + deployment.TemplateID, + status, + deployment.CreatedAt.Format(time.DateTime), + }) + count++ + } - t.Render() + if count == 0 { + log.Info().Msg("there is no running environment") + return + } - }, + t.Render() + }, + } } diff --git a/internal/cli/root.go b/internal/cli/root.go deleted file mode 100644 index 303ae6e..0000000 --- a/internal/cli/root.go +++ /dev/null @@ -1,85 +0,0 @@ -// Package cli provides command-line interface functionality for the vulnerable target application. -package cli - -import ( - "fmt" - "maps" - "slices" - "strings" - - "github.com/happyhackingspace/vulnerable-target/internal/banner" - "github.com/happyhackingspace/vulnerable-target/internal/logger" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" -) - -// LogLevels defines the valid log levels supported by the application. -var LogLevels = map[string]bool{ - zerolog.DebugLevel.String(): true, - zerolog.InfoLevel.String(): true, - zerolog.WarnLevel.String(): true, - zerolog.ErrorLevel.String(): true, - zerolog.FatalLevel.String(): true, - zerolog.PanicLevel.String(): true, -} - -// setupRootFlags configures the root command flags -func setupRootFlags() { - rootCmd.PersistentFlags().StringP("verbosity", "v", zerolog.InfoLevel.String(), - fmt.Sprintf("Set the verbosity level for logs (%s)", - strings.Join(slices.Collect(maps.Keys(LogLevels)), ", "))) -} - -var rootCmd = &cobra.Command{ - Use: "vt", - Short: "Create vulnerable environment", - Version: banner.AppVersion, - PersistentPreRun: func(cmd *cobra.Command, _ []string) { - verbosityLevel, err := cmd.Flags().GetString("verbosity") - if err != nil { - log.Fatal().Msgf("%v", err) - } - logger.InitWithLevel(verbosityLevel) - }, - SilenceErrors: true, -} - -// InitCLI initializes all CLI commands and their configurations -func InitCLI() { - // Setup root command flags - setupRootFlags() - - // Register all subcommands - registerCommands() -} - -// registerCommands registers all CLI subcommands and configures their flags -func registerCommands() { - // Register commands - rootCmd.AddCommand(startCmd) - rootCmd.AddCommand(stopCmd) - rootCmd.AddCommand(psCmd) - rootCmd.AddCommand(templateCmd) - - // Setup command-specific flags - setupStartCommand() - setupStopCommand() - setupTemplateCommand() -} - -// Run executes the root command and handles the application lifecycle. -func Run() { - // Initialize CLI before running - InitCLI() - - originalHelp := rootCmd.HelpFunc() - rootCmd.SetHelpFunc(func(c *cobra.Command, s []string) { - originalHelp(c, s) - }) - - if err := rootCmd.Execute(); err != nil { - log.Fatal().Msg(err.Error()) - } -} diff --git a/internal/cli/start.go b/internal/cli/start.go index 821f828..e4c7bdf 100644 --- a/internal/cli/start.go +++ b/internal/cli/start.go @@ -2,75 +2,76 @@ package cli import ( "fmt" - "maps" - "slices" "strings" - "github.com/happyhackingspace/vulnerable-target/pkg/provider/registry" - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + tmpl "github.com/happyhackingspace/vulnerable-target/pkg/template" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -var startCmd = &cobra.Command{ - Use: "start", - Short: "Runs selected template on chosen provider", - Run: func(cmd *cobra.Command, _ []string) { - providerName, err := cmd.Flags().GetString("provider") - if err != nil { - log.Fatal().Msgf("%v", err) - } +// newStartCommand creates the start command. +func (c *CLI) newStartCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "start", + Short: "Runs selected template on chosen provider", + Run: func(cmd *cobra.Command, _ []string) { + providerName, err := cmd.Flags().GetString("provider") + if err != nil { + log.Fatal().Msgf("%v", err) + } - templateID, err := cmd.Flags().GetString("id") - if err != nil { - log.Fatal().Msgf("%v", err) - } - provider := registry.GetProvider(providerName) - if len(templateID) == 0 { - err := cmd.Help() + templateID, err := cmd.Flags().GetString("id") if err != nil { log.Fatal().Msgf("%v", err) } - return - } - if provider == nil { - log.Fatal().Msgf("provider %s not found", providerName) - } + if len(templateID) == 0 { + if err := cmd.Help(); err != nil { + log.Fatal().Msgf("%v", err) + } + return + } - template, err := templates.GetByID(templateID) - if err != nil { - log.Fatal().Msgf("%v", err) - } + provider, ok := c.app.GetProvider(providerName) + if !ok { + log.Fatal().Msgf("provider %s not found", providerName) + } - err = provider.Start(template) - if err != nil { - log.Fatal().Msgf("%v", err) - } + template, err := tmpl.GetByID(c.app.Templates, templateID) + if err != nil { + log.Fatal().Msgf("%v", err) + } - if len(template.PostInstall) > 0 { + err = provider.Start(template) + if err != nil { + log.Fatal().Msgf("%v", err) + } - log.Info().Msg("Post-installation instructions:") - for _, instruction := range template.PostInstall { - fmt.Printf(" %s\n", instruction) + if len(template.PostInstall) > 0 { + log.Info().Msg("Post-installation instructions:") + for _, instruction := range template.PostInstall { + fmt.Printf(" %s\n", instruction) + } } - } - log.Info().Msgf("%s template is running on %s", templateID, providerName) - }, -} + log.Info().Msgf("%s template is running on %s", templateID, providerName) + }, + } -// setupStartCommand configures the start command flags -func setupStartCommand() { - startCmd.Flags().StringP("provider", "p", "docker-compose", + cmd.Flags().StringP("provider", "p", "docker-compose", fmt.Sprintf("Specify the provider for building a vulnerable environment (%s)", - strings.Join(slices.Collect(maps.Keys(registry.Providers)), ", "))) + strings.Join(c.providerNames(), ", "))) - startCmd.Flags().String("id", "", + cmd.Flags().String("id", "", "Specify a template ID for targeted vulnerable environment") - err := startCmd.MarkFlagRequired("id") - if err != nil { + if err := cmd.MarkFlagRequired("provider"); err != nil { log.Fatal().Msgf("%v", err) } + + if err := cmd.MarkFlagRequired("id"); err != nil { + log.Fatal().Msgf("%v", err) + } + + return cmd } diff --git a/internal/cli/stop.go b/internal/cli/stop.go index 5f1bbb8..73e064b 100644 --- a/internal/cli/stop.go +++ b/internal/cli/stop.go @@ -2,66 +2,62 @@ package cli import ( "fmt" - "maps" - "slices" "strings" - "github.com/happyhackingspace/vulnerable-target/pkg/provider/registry" - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + tmpl "github.com/happyhackingspace/vulnerable-target/pkg/template" "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) -var stopCmd = &cobra.Command{ - Use: "stop", - Short: "Stop vulnerable environment by template id and provider", - Run: func(cmd *cobra.Command, _ []string) { - providerName, err := cmd.Flags().GetString("provider") - if err != nil { - log.Fatal().Msgf("%v", err) - } - - templateID, err := cmd.Flags().GetString("id") - if err != nil { - log.Fatal().Msgf("%v", err) - } - - provider := registry.GetProvider(providerName) - - if provider == nil { - log.Fatal().Msgf("provider %s not found", providerName) - } - - template, err := templates.GetByID(templateID) - if err != nil { - log.Fatal().Msgf("%v", err) - } - - err = provider.Stop(template) - if err != nil { - log.Fatal().Msgf("%v", err) - } - - log.Info().Msgf("%s template stopped on %s", templateID, providerName) - }, -} +// newStopCommand creates the stop command. +func (c *CLI) newStopCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "stop", + Short: "Stop vulnerable environment by template id and provider", + Run: func(cmd *cobra.Command, _ []string) { + providerName, err := cmd.Flags().GetString("provider") + if err != nil { + log.Fatal().Msgf("%v", err) + } + + templateID, err := cmd.Flags().GetString("id") + if err != nil { + log.Fatal().Msgf("%v", err) + } + + provider, ok := c.app.GetProvider(providerName) + if !ok { + log.Fatal().Msgf("provider %s not found", providerName) + } + + template, err := tmpl.GetByID(c.app.Templates, templateID) + if err != nil { + log.Fatal().Msgf("%v", err) + } + + err = provider.Stop(template) + if err != nil { + log.Fatal().Msgf("%v", err) + } + + log.Info().Msgf("%s template stopped on %s", templateID, providerName) + }, + } -// setupStopCommand configures the stop command flags -func setupStopCommand() { - stopCmd.Flags().StringP("provider", "p", "", + cmd.Flags().StringP("provider", "p", "docker-compose", fmt.Sprintf("Specify the provider for building a vulnerable environment (%s)", - strings.Join(slices.Collect(maps.Keys(registry.Providers)), ", "))) + strings.Join(c.providerNames(), ", "))) - stopCmd.Flags().String("id", "", + cmd.Flags().String("id", "", "Specify a template ID for targeted vulnerable environment") - err := stopCmd.MarkFlagRequired("provider") - if err != nil { + if err := cmd.MarkFlagRequired("provider"); err != nil { log.Fatal().Msgf("%v", err) } - err = stopCmd.MarkFlagRequired("id") - if err != nil { + if err := cmd.MarkFlagRequired("id"); err != nil { log.Fatal().Msgf("%v", err) } + + return cmd } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index bd355c6..b95a9ea 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -2,6 +2,7 @@ package logger import ( + "io" "os" "time" @@ -9,20 +10,45 @@ import ( "github.com/rs/zerolog/log" ) -// InitWithLevel initializes the logger with the specified verbosity level. -func InitWithLevel(verbosityLevel string) { - level, err := zerolog.ParseLevel(verbosityLevel) +// Config holds logger configuration options. +type Config struct { + Level string + Output io.Writer + TimeFormat string + NoColor bool +} + +// DefaultConfig returns the default logger configuration. +func DefaultConfig() *Config { + return &Config{ + Level: "info", + Output: os.Stdout, + TimeFormat: time.TimeOnly, + NoColor: false, + } +} + +// New creates a new zerolog.Logger with the given configuration. +// This is a pure factory function that returns a configured logger +// without modifying any global state. +// If cfg is nil, DefaultConfig() is used. +func New(cfg *Config) zerolog.Logger { + if cfg == nil { + cfg = DefaultConfig() + } + + level, err := zerolog.ParseLevel(cfg.Level) if err != nil { level = zerolog.InfoLevel } output := zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: time.TimeOnly, - NoColor: false, + Out: cfg.Output, + TimeFormat: cfg.TimeFormat, + NoColor: cfg.NoColor, } - log.Logger = zerolog. + return zerolog. New(output). Level(level). With(). @@ -30,7 +56,30 @@ func InitWithLevel(verbosityLevel string) { Logger() } -// Init initializes the logger with the default info level. +// NewWithLevel creates a new zerolog.Logger with the specified level +// and default settings for other options. +func NewWithLevel(level string) zerolog.Logger { + cfg := DefaultConfig() + cfg.Level = level + return New(cfg) +} + +// SetGlobal sets the global log.Logger to the provided logger. +// This is the only function that modifies global state, and should +// be called once at application startup. +func SetGlobal(logger zerolog.Logger) { + log.Logger = logger +} + +// InitWithLevel initializes the global logger with the specified verbosity level. +// Deprecated: Use New() or NewWithLevel() with SetGlobal() instead. +func InitWithLevel(verbosityLevel string) { + logger := NewWithLevel(verbosityLevel) + SetGlobal(logger) +} + +// Init initializes the global logger with the default info level. +// Deprecated: Use New() or NewWithLevel() with SetGlobal() instead. func Init() { InitWithLevel("info") } diff --git a/pkg/provider/dockercompose/dockercompose.go b/pkg/provider/dockercompose/dockercompose.go index 174cb9c..1affdb3 100644 --- a/pkg/provider/dockercompose/dockercompose.go +++ b/pkg/provider/dockercompose/dockercompose.go @@ -6,14 +6,20 @@ import ( "github.com/happyhackingspace/vulnerable-target/internal/state" "github.com/happyhackingspace/vulnerable-target/pkg/provider" - "github.com/happyhackingspace/vulnerable-target/pkg/store/disk" - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + tmpl "github.com/happyhackingspace/vulnerable-target/pkg/template" ) var _ provider.Provider = &DockerCompose{} // DockerCompose implements the Provider interface using Docker Compose. -type DockerCompose struct{} +type DockerCompose struct { + stateManager *state.Manager +} + +// NewDockerCompose creates a new DockerCompose provider with the given state manager. +func NewDockerCompose(sm *state.Manager) *DockerCompose { + return &DockerCompose{stateManager: sm} +} // Name returns the provider name. func (d *DockerCompose) Name() string { @@ -21,14 +27,8 @@ func (d *DockerCompose) Name() string { } // Start launches the vulnerable target environment using Docker Compose. -func (d *DockerCompose) Start(template *templates.Template) error { - cfg := disk.NewConfig().WithFileName("deployments.db").WithBucketName("deployments") - st, err := state.NewManager(cfg) - if err != nil { - return err - } - - exist, _ := st.DeploymentExist(d.Name(), template.ID) //nolint:errcheck +func (d *DockerCompose) Start(template *tmpl.Template) error { + exist, _ := d.stateManager.DeploymentExist(d.Name(), template.ID) //nolint:errcheck if exist { return fmt.Errorf("already running") } @@ -48,7 +48,7 @@ func (d *DockerCompose) Start(template *templates.Template) error { return err } - err = st.AddNewDeployment(d.Name(), template.ID) + err = d.stateManager.AddNewDeployment(d.Name(), template.ID) if err != nil { return err } @@ -57,14 +57,8 @@ func (d *DockerCompose) Start(template *templates.Template) error { } // Stop shuts down the vulnerable target environment using Docker Compose. -func (d *DockerCompose) Stop(template *templates.Template) error { - cfg := disk.NewConfig().WithFileName("deployments.db").WithBucketName("deployments") - st, err := state.NewManager(cfg) - if err != nil { - return err - } - - exist, err := st.DeploymentExist(d.Name(), template.ID) +func (d *DockerCompose) Stop(template *tmpl.Template) error { + exist, err := d.stateManager.DeploymentExist(d.Name(), template.ID) if err != nil { return err } @@ -88,7 +82,7 @@ func (d *DockerCompose) Stop(template *templates.Template) error { return err } - err = st.RemoveDeployment(d.Name(), template.ID) + err = d.stateManager.RemoveDeployment(d.Name(), template.ID) if err != nil { return err } @@ -97,7 +91,7 @@ func (d *DockerCompose) Stop(template *templates.Template) error { } // Status returns status the vulnerable target environment using Docker Compose. -func (d *DockerCompose) Status(template *templates.Template) (string, error) { +func (d *DockerCompose) Status(template *tmpl.Template) (string, error) { dockerCli, err := createDockerCLI() if err != nil { return "unknown", err diff --git a/pkg/provider/dockercompose/utils.go b/pkg/provider/dockercompose/utils.go index ac4c54b..9e4f32e 100644 --- a/pkg/provider/dockercompose/utils.go +++ b/pkg/provider/dockercompose/utils.go @@ -2,9 +2,11 @@ package dockercompose import ( "context" + "errors" "fmt" "os" "path/filepath" + "strings" "time" "github.com/compose-spec/compose-go/v2/loader" @@ -13,7 +15,8 @@ import ( "github.com/docker/cli/cli/flags" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/compose" - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + "github.com/happyhackingspace/vulnerable-target/internal/app" + tmpl "github.com/happyhackingspace/vulnerable-target/pkg/template" ) func createDockerCLI() (command.Cli, error) { @@ -31,7 +34,7 @@ func createDockerCLI() (command.Cli, error) { return dockerCli, nil } -func loadComposeProject(template templates.Template) (*types.Project, error) { +func loadComposeProject(template tmpl.Template) (*types.Project, error) { providerConfig, exists := template.Providers["docker-compose"] if !exists { return nil, fmt.Errorf("template %q missing docker-compose provider configuration", template.ID) @@ -40,7 +43,7 @@ func loadComposeProject(template templates.Template) (*types.Project, error) { return nil, fmt.Errorf("template %q docker-compose.path is empty", template.ID) } - composePath, workingDir, err := resolveComposePath(template.ID, providerConfig.Path) + composePath, workingDir, err := resolveComposePath(template.ID, template.Info.Type, providerConfig.Path) if err != nil { return nil, err } @@ -165,17 +168,22 @@ func runComposeStats(dockerCli command.Cli, project *types.Project) (bool, error return true, nil } -func resolveComposePath(templateID, path string) (composePath string, workingDir string, err error) { +func resolveComposePath(templateID, templateType, path string) (composePath string, workingDir string, err error) { if filepath.IsAbs(path) { return path, filepath.Dir(path), nil } + var categoryMap = map[string]string{ + "lab": "labs", + "cve": "cves", + } - wd, err := os.Getwd() - if err != nil { - return "", "", err + category, ctgExist := categoryMap[strings.ToLower(templateType)] + if !ctgExist { + return "", "", errors.New("undefined category for template: type must be one of 'lab', 'cve'") } - composePath = filepath.Join(wd, "templates", templateID, path) + cfg := app.DefaultConfig() + composePath = filepath.Join(cfg.TemplatesPath, category, templateID, path) if _, statErr := os.Stat(composePath); statErr != nil { return "", "", fmt.Errorf("compose file %q not accessible: %w", composePath, statErr) diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 54ca148..e114b7f 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -2,13 +2,13 @@ package provider import ( - "github.com/happyhackingspace/vulnerable-target/pkg/templates" + tmpl "github.com/happyhackingspace/vulnerable-target/pkg/template" ) // Provider defines the interface for managing vulnerable target environments. type Provider interface { Name() string - Start(template *templates.Template) error - Stop(template *templates.Template) error - Status(template *templates.Template) (string, error) + Start(template *tmpl.Template) error + Stop(template *tmpl.Template) error + Status(template *tmpl.Template) (string, error) } diff --git a/pkg/provider/registry/registry.go b/pkg/provider/registry/registry.go index 9368108..a384c29 100644 --- a/pkg/provider/registry/registry.go +++ b/pkg/provider/registry/registry.go @@ -2,16 +2,21 @@ package registry import ( + "github.com/happyhackingspace/vulnerable-target/internal/state" "github.com/happyhackingspace/vulnerable-target/pkg/provider" "github.com/happyhackingspace/vulnerable-target/pkg/provider/dockercompose" ) -// Providers contains all available providers registered in the system. -var Providers = map[string]provider.Provider{ - "docker-compose": &dockercompose.DockerCompose{}, +// NewProviders creates and returns a map of all available providers. +// Each provider is initialized with the given state manager. +func NewProviders(sm *state.Manager) map[string]provider.Provider { + return map[string]provider.Provider{ + "docker-compose": dockercompose.NewDockerCompose(sm), + } } -// GetProvider returns the provider with the specified name. -func GetProvider(name string) provider.Provider { - return Providers[name] +// GetProvider retrieves a provider by name from the given providers map. +func GetProvider(providers map[string]provider.Provider, name string) (provider.Provider, bool) { + p, ok := providers[name] + return p, ok } diff --git a/pkg/templates/downloader.go b/pkg/template/downloader.go similarity index 95% rename from pkg/templates/downloader.go rename to pkg/template/downloader.go index 0e485f1..332194b 100644 --- a/pkg/templates/downloader.go +++ b/pkg/template/downloader.go @@ -1,4 +1,4 @@ -package templates +package template import ( "errors" @@ -47,7 +47,7 @@ func cloneTemplatesRepo(repoPath string, force bool) error { } _, err = git.PlainClone(repoPath, false, &git.CloneOptions{ - URL: TemplateRemoteRepoistory, + URL: TemplateRemoteRepository, Depth: 1, }) diff --git a/pkg/templates/parser.go b/pkg/template/parser.go similarity index 78% rename from pkg/templates/parser.go rename to pkg/template/parser.go index 41f384d..23b8988 100644 --- a/pkg/templates/parser.go +++ b/pkg/template/parser.go @@ -1,5 +1,5 @@ -// Package templates provides functionality for loading and managing vulnerable target environment templates. -package templates +// Package template provides functionality for loading and managing vulnerable target environment templates. +package template import ( "os" diff --git a/pkg/templates/parser_test.go b/pkg/template/parser_test.go similarity index 98% rename from pkg/templates/parser_test.go rename to pkg/template/parser_test.go index c5a2dfd..5d9fba1 100644 --- a/pkg/templates/parser_test.go +++ b/pkg/template/parser_test.go @@ -1,4 +1,4 @@ -package templates +package template import ( "os" diff --git a/pkg/templates/template.go b/pkg/template/template.go similarity index 56% rename from pkg/templates/template.go rename to pkg/template/template.go index 50a00c5..1cdeef8 100644 --- a/pkg/templates/template.go +++ b/pkg/template/template.go @@ -1,4 +1,4 @@ -package templates +package template import ( "fmt" @@ -10,8 +10,8 @@ import ( "github.com/rs/zerolog/log" ) -// TemplateRemoteRepoistory is a constant for repo url. -const TemplateRemoteRepoistory string = "https://github.com/HappyHackingSpace/vt-templates" +// TemplateRemoteRepository is a constant for repo url. +const TemplateRemoteRepository string = "https://github.com/HappyHackingSpace/vt-templates" // Template represents a vulnerable target environment configuration. type Template struct { @@ -49,33 +49,28 @@ type Cvss struct { Metrics string `yaml:"metrics"` } -// Templates contains all loaded templates indexed by their ID. -var Templates = make(map[string]Template) - -// Init initializes the templates repository and loads all templates. -func Init() { - homeDir, err := os.UserHomeDir() - if err != nil { - log.Fatal().Msgf("%v", err) - } - - repoPath := filepath.Join(homeDir, "vt-templates") +// LoadTemplates loads all templates from the given repository path. +// If the repository doesn't exist, it clones it first. +// Returns a map of templates indexed by their ID. +func LoadTemplates(repoPath string) (map[string]Template, error) { if _, err := os.Stat(repoPath); os.IsNotExist(err) { log.Info().Msg("Fetching templates for the first time") - err = cloneTemplatesRepo(repoPath, false) - if err != nil { - log.Fatal().Msgf("%v", err) + if err := cloneTemplatesRepo(repoPath, false); err != nil { + return nil, fmt.Errorf("failed to clone templates repository: %w", err) } } - loadTemplatesFromDirectory(repoPath) + return loadTemplatesFromDirectory(repoPath) } -// loadTemplatesFromDirectory reads all templates from the given path and populates the Templates map. -func loadTemplatesFromDirectory(repoPath string) { +// loadTemplatesFromDirectory reads all templates from the given path. +// Returns a map of templates indexed by their ID. +func loadTemplatesFromDirectory(repoPath string) (map[string]Template, error) { + templates := make(map[string]Template) + dirEntry, err := os.ReadDir(repoPath) if err != nil { - log.Fatal().Msgf("%v", err) + return nil, fmt.Errorf("failed to read directory %s: %w", repoPath, err) } for _, categoryEntry := range dirEntry { @@ -84,15 +79,27 @@ func loadTemplatesFromDirectory(repoPath string) { } categoryPath := filepath.Join(repoPath, categoryEntry.Name()) - loadTemplatesFromCategory(categoryPath, categoryEntry.Name()) + categoryTemplates, err := loadTemplatesFromCategory(categoryPath, categoryEntry.Name()) + if err != nil { + return nil, err + } + + for id, tmpl := range categoryTemplates { + templates[id] = tmpl + } } + + return templates, nil } // loadTemplatesFromCategory loads all templates within a single category directory. -func loadTemplatesFromCategory(categoryPath, categoryName string) { +// Returns a map of templates indexed by their ID. +func loadTemplatesFromCategory(categoryPath, categoryName string) (map[string]Template, error) { + templates := make(map[string]Template) + templateEntries, err := os.ReadDir(categoryPath) if err != nil { - log.Fatal().Msgf("Error reading category %s: %v", categoryName, err) + return nil, fmt.Errorf("error reading category %s: %w", categoryName, err) } for _, entry := range templateEntries { @@ -101,49 +108,45 @@ func loadTemplatesFromCategory(categoryPath, categoryName string) { } templatePath := filepath.Join(categoryPath, entry.Name()) - template, err := LoadTemplate(templatePath) + tmpl, err := LoadTemplate(templatePath) if err != nil { - log.Fatal().Msgf("Error loading template %s: %v", entry.Name(), err) + return nil, fmt.Errorf("error loading template %s: %w", entry.Name(), err) } - if template.ID != entry.Name() { - log.Fatal().Msgf("id and directory name should match") + if tmpl.ID != entry.Name() { + return nil, fmt.Errorf("template id '%s' and directory name '%s' should match", tmpl.ID, entry.Name()) } - Templates[template.ID] = template + templates[tmpl.ID] = tmpl } + + return templates, nil } // SyncTemplates downloads or updates all templates from the remote repository. -func SyncTemplates() { - homeDir, err := os.UserHomeDir() - if err != nil { - log.Fatal().Msgf("%v", err) - } - - repoPath := filepath.Join(homeDir, "vt-templates") - log.Info().Msgf("cloning %s", TemplateRemoteRepoistory) - err = cloneTemplatesRepo(repoPath, true) - if err != nil { - log.Fatal().Msgf("%v", err) +func SyncTemplates(repoPath string) error { + log.Info().Msgf("cloning %s", TemplateRemoteRepository) + if err := cloneTemplatesRepo(repoPath, true); err != nil { + return fmt.Errorf("failed to sync templates: %w", err) } + return nil } -// List displays all available templates in a table format. -func List() { - ListWithFilter("") +// ListTemplates displays all available templates in a table format. +func ListTemplates(templates map[string]Template) { + ListTemplatesWithFilter(templates, "") } -// ListWithFilter displays all available templates in a table format, optionally filtered by tag. -func ListWithFilter(filterTag string) { +// ListTemplatesWithFilter displays templates in a table format, optionally filtered by tag. +func ListTemplatesWithFilter(templates map[string]Template, filterTag string) { t := table.NewWriter() t.SetStyle(table.StyleDefault) t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"ID", "Name", "Author", "Targets", "Type", "Tags"}) count := 0 - for _, template := range Templates { + for _, tmpl := range templates { if filterTag != "" { hasTag := false - for _, tag := range template.Info.Tags { + for _, tag := range tmpl.Info.Tags { if strings.EqualFold(tag, filterTag) || strings.Contains(strings.ToLower(tag), strings.ToLower(filterTag)) { hasTag = true break @@ -154,14 +157,14 @@ func ListWithFilter(filterTag string) { } } - tags := strings.Join(template.Info.Tags, ", ") - targets := strings.Join(template.Info.Targets, ", ") + tags := strings.Join(tmpl.Info.Tags, ", ") + targets := strings.Join(tmpl.Info.Targets, ", ") t.AppendRow(table.Row{ - template.ID, - template.Info.Name, - template.Info.Author, + tmpl.ID, + tmpl.Info.Name, + tmpl.Info.Author, targets, - template.Info.Type, + tmpl.Info.Type, tags, }) count++ @@ -185,11 +188,11 @@ func ListWithFilter(filterTag string) { t.Render() } -// GetByID retrieves a template by its ID. -func GetByID(templateID string) (*Template, error) { - template := Templates[templateID] - if template.ID == "" { +// GetByID retrieves a template by its ID from the given templates map. +func GetByID(templates map[string]Template, templateID string) (*Template, error) { + tmpl, ok := templates[templateID] + if !ok || tmpl.ID == "" { return nil, fmt.Errorf("template %s not found", templateID) } - return &template, nil + return &tmpl, nil } diff --git a/pkg/templates/template_test.go b/pkg/template/template_test.go similarity index 76% rename from pkg/templates/template_test.go rename to pkg/template/template_test.go index a1119bc..18440ec 100644 --- a/pkg/templates/template_test.go +++ b/pkg/template/template_test.go @@ -1,4 +1,4 @@ -package templates +package template import ( "fmt" @@ -8,7 +8,7 @@ import ( ) func TestGetByID(t *testing.T) { - Templates = map[string]Template{ + templates := map[string]Template{ "example-template-1": { ID: "example-template-1", Info: Info{ @@ -21,13 +21,12 @@ func TestGetByID(t *testing.T) { }, }, } - firstTemplate, err := GetByID("example-template-1") + firstTemplate, err := GetByID(templates, "example-template-1") assert.Nil(t, err) assert.Equal(t, "example-template-1", firstTemplate.ID) noneExistTemplateID := "none-exist-template" - noneExistingTemplate, err := GetByID(noneExistTemplateID) + noneExistingTemplate, err := GetByID(templates, noneExistTemplateID) assert.Nil(t, noneExistingTemplate) assert.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("template %s not found", noneExistTemplateID)) - } diff --git a/pkg/templates/validator.go b/pkg/template/validator.go similarity index 99% rename from pkg/templates/validator.go rename to pkg/template/validator.go index f148ea0..65a7b0e 100644 --- a/pkg/templates/validator.go +++ b/pkg/template/validator.go @@ -1,4 +1,4 @@ -package templates +package template import ( "fmt"