Skip to content
Merged

dev #204

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
61 changes: 58 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Spin up vulnerable targets from your terminal 🎯
- [Quick Start](#quick-start)
- [Usage](#usage)
- [Templates](#templates)
- [Playbooks](#playbooks)
- [What can you do with vt?](#what-can-you-do-with-vt)
- [Documentation](#documentation)
- [Star History](#star-history)
Expand All @@ -38,7 +39,9 @@ Spin up vulnerable targets from your terminal 🎯
|:--:|---------|-------------|
| 🐳 | **Docker Compose** | Container orchestration for vulnerable environments |
| 📦 | **Templates** | Community-curated vulnerable targets from [vt-templates](https://github.com/HappyHackingSpace/vt-templates) |
| 📓 | **Playbooks** | Group multiple templates into training scenarios and run them together |
| 📊 | **State Tracking** | Track and manage running deployments |
| 🔍 | **Inspect** | View detailed info (CVE, CVSS, CWE, PoC, remediation) for any template or playbook |
| 🔄 | **Auto-Update** | Sync templates from remote repository |

---
Expand All @@ -47,7 +50,7 @@ Spin up vulnerable targets from your terminal 🎯

### Prerequisites

- Go 1.24+
- Go 1.25.6+
- Docker & Docker Compose

### Install with Go
Expand Down Expand Up @@ -86,15 +89,36 @@ vt start --id vt-dvwa
<details>
<summary><b>Command Reference</b></summary>

**Templates**

| Command | Description |
|---------|-------------|
| `vt template --list` | List all available templates |
| `vt template --list --filter <tag>` | Filter templates by tag |
| `vt template --update` | Update templates from remote repository |

**Environments**

| Command | Description |
|---------|-------------|
| `vt start --id <template-id>` | Start a vulnerable environment |
| `vt ps` | List running environments |
| `vt stop --id <template-id>` | Stop an environment |
| `vt -v debug <command>` | Run with debug verbosity |
| `vt ps` | List all running environments |
| `vt inspect --id <template-id>` | Show full details for a template |

**Playbooks**

| Command | Description |
|---------|-------------|
| `vt playbook list` | List all available playbooks |
| `vt playbook run --id <playbook-id>` | Start all templates in a playbook |
| `vt playbook stop --id <playbook-id>` | Stop all templates in a playbook |

**Global Flags**

| Flag | Values | Description |
|------|--------|-------------|
| `-v, --verbosity` | `debug` `info` `warn` `error` `fatal` `panic` | Set log verbosity (default: `info`) |

</details>

Expand All @@ -107,11 +131,23 @@ vt template --list --filter sqli
# Start DVWA (Damn Vulnerable Web App)
vt start --id vt-dvwa

# Inspect a template — see CVE, CVSS, CWE, PoC, and remediation steps
vt inspect --id vt-dvwa

# Check running environments
vt ps

# Stop a specific environment
vt stop --id vt-dvwa

# Run an entire playbook (multiple targets at once)
vt playbook run --id vt-pb-1

# List all available playbooks
vt playbook list

# Stop all targets in a playbook
vt playbook stop --id vt-pb-1
```

---
Expand All @@ -132,6 +168,25 @@ Templates are automatically cloned to `~/vt-templates` on first run.

---

## Playbooks

Playbooks let you start multiple vulnerable targets in one command — useful for structured training sessions or red-team labs that require several services running simultaneously.

```bash
# See what playbooks are available
vt playbook list

# Launch every target in a playbook
vt playbook run --id vt-pb-1

# Tear down the whole playbook when done
vt playbook stop --id vt-pb-1
```

Playbooks are defined as YAML files in the `playbooks/` directory of the templates repository. Each playbook specifies an ordered list of template IDs. If one template fails to start, `vt` skips it, continues with the rest, and reports a summary of failures at the end.

---

## What can you do with vt?

| Use Case | Template |
Expand Down
4 changes: 2 additions & 2 deletions cmd/vt/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ func main() {
appLogger := logger.NewWithLevel(cfg.LogLevel)
logger.SetGlobal(appLogger)

templates, err := template.LoadTemplates(cfg.TemplatesPath)
templates, playbooks, err := template.LoadRepo(cfg.TemplatesPath)
if err != nil {
log.Fatal().Err(err).Msg("failed to load templates")
}

providers := registry.NewProviders()

application := app.NewApp(templates, providers, cfg)
application := app.NewApp(templates, playbooks, providers, cfg)

if err := cli.New(application).Run(); err != nil {
log.Fatal().Err(err).Msg("CLI error")
Expand Down
3 changes: 3 additions & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Config struct {
// App is the dependency container for the application.
type App struct {
Templates map[string]template.Template
Playbooks map[string]template.Playbook
Providers map[string]provider.Provider
Config *Config
}
Expand All @@ -40,11 +41,13 @@ func DefaultConfig() *Config {
// NewApp creates a new App instance with the given dependencies.
func NewApp(
templates map[string]template.Template,
playbooks map[string]template.Playbook,
providers map[string]provider.Provider,
config *Config,
) *App {
return &App{
Templates: templates,
Playbooks: playbooks,
Providers: providers,
Config: config,
}
Expand Down
1 change: 1 addition & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func (c *CLI) setupCommands() {
c.rootCmd.AddCommand(c.newPsCommand())
c.rootCmd.AddCommand(c.newTemplateCommand())
c.rootCmd.AddCommand(c.newInspectCommand())
c.rootCmd.AddCommand(c.newPlaybookCommand())
}

// Run executes the CLI and returns any error.
Expand Down
13 changes: 9 additions & 4 deletions internal/cli/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ func (c *CLI) newInspectCommand() *cobra.Command {
log.Fatal().Msgf("%v", err)
}

template, err := templ.GetByID(c.app.Templates, templateID)
if err != nil {
log.Fatal().Msgf("%v", err)
if template, err := templ.GetByID(c.app.Templates, templateID); err == nil {
fmt.Println(template.String())
return
}

if pb, err := templ.GetPlaybookByID(c.app.Playbooks, templateID); err == nil {
fmt.Println(pb.String())
return
}

fmt.Println(template.String())
log.Fatal().Msgf("'%s' not found as a template or playbook", templateID)
},
}
cmd.Flags().String("id", "", "Specify a template ID for targeted vulnerable environment")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update --id help text to match new playbook support.

The flag description still says template-only, but the command now accepts both template and playbook IDs.

Proposed fix
-	cmd.Flags().String("id", "", "Specify a template ID for targeted vulnerable environment")
+	cmd.Flags().String("id", "", "Specify a template or playbook ID for targeted vulnerable environment")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cmd.Flags().String("id", "", "Specify a template ID for targeted vulnerable environment")
cmd.Flags().String("id", "", "Specify a template or playbook ID for targeted vulnerable environment")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/inspect.go` at line 34, The flag description for the "id" flag
(registered via cmd.Flags().String("id", ... ) in internal/cli/inspect.go) is
outdated — update the help text to reflect that the command accepts both
template and playbook IDs (e.g., change "Specify a template ID for targeted
vulnerable environment" to something like "Specify a template or playbook ID for
targeted vulnerable environment") so users see the correct supported inputs.

Expand Down
168 changes: 168 additions & 0 deletions internal/cli/playbook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package cli

import (
"fmt"
"strings"

tmpl "github.com/happyhackingspace/vt/pkg/template"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

// newPlaybookCommand creates the playbook command with its subcommands.
func (c *CLI) newPlaybookCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "playbook",
Short: "Manage and run playbooks (collections of templates)",
}

cmd.AddCommand(c.newPlaybookRunCommand())
cmd.AddCommand(c.newPlaybookStopCommand())
cmd.AddCommand(c.newPlaybookListCommand())
return cmd
}

// newPlaybookRunCommand creates the playbook run subcommand.
func (c *CLI) newPlaybookRunCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "run",
Short: "Run all templates defined in a playbook file",
Run: func(cmd *cobra.Command, _ []string) {
playbookID, err := cmd.Flags().GetString("id")
if err != nil {
log.Fatal().Msgf("%v", err)
}

providerName, err := cmd.Flags().GetString("provider")
if err != nil {
log.Fatal().Msgf("%v", err)
}

provider, ok := c.app.GetProvider(providerName)
if !ok {
log.Fatal().Msgf("provider %s not found", providerName)
}

pb, err := tmpl.GetPlaybookByID(c.app.Playbooks, playbookID)
if err != nil {
log.Fatal().Msgf("%v", err)
}

log.Info().Msgf("running playbook '%s' [%s] (%d templates)", pb.Info.Name, pb.ID, len(pb.Templates))

var failed []string
for _, templateID := range pb.Templates {
template, err := tmpl.GetByID(c.app.Templates, templateID)
if err != nil {
log.Error().Msgf("skipping '%s': %v", templateID, err)
failed = append(failed, templateID)
continue
}

log.Info().Msgf("starting template '%s'", templateID)
if err := provider.Start(template); err != nil {
log.Error().Msgf("failed to start '%s': %v", templateID, err)
failed = append(failed, templateID)
continue
}

if len(template.PostInstall) > 0 {
log.Info().Msgf("post-install instructions for '%s':", templateID)
for _, instruction := range template.PostInstall {
fmt.Printf(" %s\n", instruction)
}
}
}

if len(failed) > 0 {
log.Warn().Msgf("playbook finished with errors — failed templates: %s", strings.Join(failed, ", "))
} else {
log.Info().Msgf("playbook '%s' completed successfully", pb.Info.Name)
}
Comment on lines +77 to +81

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Return non-zero exit when any playbook item fails.

Both run and stop currently report partial failures with warnings but still exit successfully. That masks operational failure in scripts and CI.

Proposed fix
 			if len(failed) > 0 {
-				log.Warn().Msgf("playbook finished with errors — failed templates: %s", strings.Join(failed, ", "))
+				log.Fatal().Msgf("playbook finished with errors — failed templates: %s", strings.Join(failed, ", "))
 			} else {
 				log.Info().Msgf("playbook '%s' completed successfully", pb.Info.Name)
 			}
 			if len(failed) > 0 {
-				log.Warn().Msgf("playbook stop finished with errors — failed templates: %s", strings.Join(failed, ", "))
+				log.Fatal().Msgf("playbook stop finished with errors — failed templates: %s", strings.Join(failed, ", "))
 			} else {
 				log.Info().Msgf("playbook '%s' stopped successfully", pb.Info.Name)
 			}

Also applies to: 140-144

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/playbook.go` around lines 77 - 81, The code currently logs
partial failures for playbook execution but still returns success; update the
failure handling in the playbook command so any non-empty failed slice causes a
non-zero exit or returned error: replace the log.Warn branch in the run/stop
handlers (the blocks referencing failed and pb.Info.Name) to log the error and
then return an error (e.g., fmt.Errorf("playbook finished with errors: %v",
failed) ) or call os.Exit(1) depending on the function signature; apply the same
change to both occurrences (the blocks around failed, pb.Info.Name and the
second block reported in the comment) so CI/scripts see a non-zero exit on
partial failures.

},
}

cmd.Flags().String("id", "", "Specify a playbook ID to run")
cmd.Flags().StringP("provider", "p", "docker-compose",
fmt.Sprintf("Specify the provider (%s)", strings.Join(c.providerNames(), ", ")))

if err := cmd.MarkFlagRequired("id"); err != nil {
log.Fatal().Msgf("%v", err)
}

return cmd
}

// newPlaybookStopCommand creates the playbook stop subcommand.
func (c *CLI) newPlaybookStopCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "stop",
Short: "Stop all templates defined in a playbook",
Run: func(cmd *cobra.Command, _ []string) {
playbookID, err := cmd.Flags().GetString("id")
if err != nil {
log.Fatal().Msgf("%v", err)
}

providerName, err := cmd.Flags().GetString("provider")
if err != nil {
log.Fatal().Msgf("%v", err)
}

provider, ok := c.app.GetProvider(providerName)
if !ok {
log.Fatal().Msgf("provider %s not found", providerName)
}

pb, err := tmpl.GetPlaybookByID(c.app.Playbooks, playbookID)
if err != nil {
log.Fatal().Msgf("%v", err)
}

log.Info().Msgf("stopping playbook '%s' [%s] (%d templates)", pb.Info.Name, pb.ID, len(pb.Templates))

var failed []string
for _, templateID := range pb.Templates {
template, err := tmpl.GetByID(c.app.Templates, templateID)
if err != nil {
log.Error().Msgf("skipping '%s': %v", templateID, err)
failed = append(failed, templateID)
continue
}

log.Info().Msgf("stopping template '%s'", templateID)
if err := provider.Stop(template); err != nil {
log.Error().Msgf("failed to stop '%s': %v", templateID, err)
failed = append(failed, templateID)
}
}

if len(failed) > 0 {
log.Warn().Msgf("playbook stop finished with errors — failed templates: %s", strings.Join(failed, ", "))
} else {
log.Info().Msgf("playbook '%s' stopped successfully", pb.Info.Name)
}
},
}

cmd.Flags().String("id", "", "Specify a playbook ID to stop")
cmd.Flags().StringP("provider", "p", "docker-compose",
fmt.Sprintf("Specify the provider (%s)", strings.Join(c.providerNames(), ", ")))

if err := cmd.MarkFlagRequired("id"); err != nil {
log.Fatal().Msgf("%v", err)
}

return cmd
}

// newPlaybookListCommand creates the playbook list subcommand.
func (c *CLI) newPlaybookListCommand() *cobra.Command {
return &cobra.Command{
Use: "list",
Short: "List all available playbooks",
Run: func(_ *cobra.Command, _ []string) {
tmpl.ListPlaybooks(c.app.Playbooks)
},
}
}
5 changes: 2 additions & 3 deletions internal/cli/list.go → internal/cli/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,13 @@ func (c *CLI) newTemplateCommand() *cobra.Command {
log.Error().Err(err).Msg("failed to sync templates")
return
}
// Reload templates after sync
templates, err := tmpl.LoadTemplates(c.app.Config.TemplatesPath)
templates, playbooks, err := tmpl.LoadRepo(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
c.app.Playbooks = playbooks
log.Info().Msg("Templates updated successfully")
return
}
Expand Down
Loading
Loading