Complete rewrite and re-architecture Osmedeus Engine in v5

This commit is contained in:
j3ssie
2026-01-18 19:32:24 +08:00
commit 7a2c5a5dc9
743 changed files with 99767 additions and 0 deletions
+1133
View File
File diff suppressed because it is too large Load Diff
+739
View File
@@ -0,0 +1,739 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/charmbracelet/glamour"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/database"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/spf13/cobra"
)
var (
dbForce bool
dbTable string
dbOffset int
dbLimit int
dbJSON bool
dbNoTUI bool
dbWhere []string
dbColumns string
dbSearch string
dbWidth int
dbAll bool
dbIndexForce bool
dbListColumns bool
dbExcludeColumns string
)
// defaultHiddenColumns are columns hidden by default for all tables
var defaultHiddenColumns = []string{"id", "created_at", "updated_at", "completed_at"}
// tableDefaultColumns defines default columns for specific tables
var tableDefaultColumns = map[string][]string{
"assets": {"host", "host_ip", "title", "status_code", "words", "tech"},
}
// dbCmd - parent command for database management
var dbCmd = &cobra.Command{
Use: "db",
Short: "Database management commands",
Long: UsageDB(),
}
// dbSeedCmd - seed database with sample data
var dbSeedCmd = &cobra.Command{
Use: "seed",
Short: "Seed database with sample data",
Long: UsageDBSeed(),
RunE: runDBSeed,
}
// dbCleanCmd - clean all data from database
var dbCleanCmd = &cobra.Command{
Use: "clean",
Short: "Remove all data from database",
Long: UsageDBClean(),
RunE: runDBClean,
}
// dbMigrateCmd - run database migrations
var dbMigrateCmd = &cobra.Command{
Use: "migrate",
Short: "Run database migrations",
Long: UsageDBMigrate(),
RunE: runDBMigrate,
}
// dbListCmd - list database tables
var dbListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List database tables and row counts",
Long: UsageDBList(),
RunE: runDBList,
}
// dbIndexCmd - parent command for indexing resources
var dbIndexCmd = &cobra.Command{
Use: "index",
Short: "Index resources from filesystem to database",
Long: `Index resources from the filesystem into the database for faster querying.`,
}
// dbIndexWorkflowCmd - index workflows from filesystem
var dbIndexWorkflowCmd = &cobra.Command{
Use: "workflow",
Short: "Index workflows from filesystem to database",
Long: `Scan the workflows directory and index all workflows into the database.
This enables faster workflow listing and filtering by tags, kind, etc.
The command will:
- Add new workflows found on disk
- Update workflows that have changed (checksum mismatch)
- Remove workflows from DB that no longer exist on disk
Use --force to re-index all workflows regardless of checksum.`,
RunE: runIndexWorkflows,
}
func init() {
dbCleanCmd.Flags().BoolVar(&dbForce, "force", false, "skip confirmation prompt")
dbListCmd.Flags().StringVarP(&dbTable, "table", "t", "", "table name to list records from (runs, step_results, artifacts, assets, event_logs, schedules, workspaces)")
dbListCmd.Flags().IntVar(&dbOffset, "offset", 0, "number of records to skip (for pagination)")
dbListCmd.Flags().IntVar(&dbLimit, "limit", 50, "maximum number of records to return")
dbListCmd.Flags().BoolVar(&dbJSON, "json", false, "output records as JSON only (no extra output, bypasses TUI)")
dbListCmd.Flags().BoolVar(&dbNoTUI, "no-tui", false, "disable interactive TUI mode, use plain text output")
dbListCmd.Flags().StringArrayVar(&dbWhere, "where", nil, "filter records (key=value format, can be repeated) - only with --no-tui")
dbListCmd.Flags().StringVar(&dbColumns, "columns", "", "comma-separated columns to display (default: all) - only with --no-tui")
dbListCmd.Flags().StringVar(&dbSearch, "search", "", "search all columns for substring (case-insensitive) - only with --no-tui")
dbListCmd.Flags().IntVar(&dbWidth, "width", 30, "max column width for table display (0 = no limit) - only with --no-tui")
dbListCmd.Flags().BoolVar(&dbAll, "all", false, "show all columns including hidden ones (id, timestamps) - only with --no-tui")
dbListCmd.Flags().BoolVar(&dbListColumns, "list-columns", false, "list all available columns for the specified table")
dbListCmd.Flags().StringVar(&dbExcludeColumns, "exclude-columns", "", "comma-separated column names to exclude from output")
dbIndexWorkflowCmd.Flags().BoolVar(&dbIndexForce, "force", false, "force re-index all workflows regardless of checksum")
dbIndexCmd.AddCommand(dbIndexWorkflowCmd)
dbCmd.AddCommand(dbSeedCmd)
dbCmd.AddCommand(dbCleanCmd)
dbCmd.AddCommand(dbMigrateCmd)
dbCmd.AddCommand(dbListCmd)
dbCmd.AddCommand(dbIndexCmd)
}
// runDBSeed seeds the database with sample data
func runDBSeed(cmd *cobra.Command, args []string) error {
if disableDB {
return fmt.Errorf("database commands unavailable: --disable-db flag is set")
}
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
printer.Info("Connecting to database...")
// Connect to database
db, err := database.Connect(cfg)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer func() { _ = database.Close() }()
// Run migrations first to ensure tables exist
ctx := context.Background()
if err := database.Migrate(ctx); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
printer.Info("Seeding database with sample data...")
// Seed the database
if err := database.SeedDatabase(ctx); err != nil {
return fmt.Errorf("failed to seed database: %w", err)
}
printer.Success("Database seeded successfully")
printer.Info("Database: %s", getDatabaseInfo(cfg, db))
return nil
}
// runDBClean removes all data from the database
func runDBClean(cmd *cobra.Command, args []string) error {
if disableDB {
return fmt.Errorf("database commands unavailable: --disable-db flag is set")
}
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
if !dbForce {
printer.Warning("This will delete ALL data from the database!")
printer.Warning("Use --force to skip this confirmation")
return fmt.Errorf("operation aborted: use --force to confirm")
}
printer.Info("Connecting to database...")
// Connect to database
db, err := database.Connect(cfg)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer func() { _ = database.Close() }()
ctx := context.Background()
printer.Info("Cleaning database...")
// Clean the database
if err := database.CleanDatabase(ctx); err != nil {
return fmt.Errorf("failed to clean database: %w", err)
}
printer.Success("Database cleaned successfully")
printer.Info("Database: %s", getDatabaseInfo(cfg, db))
return nil
}
// runDBMigrate runs database migrations
func runDBMigrate(cmd *cobra.Command, args []string) error {
if disableDB {
return fmt.Errorf("database commands unavailable: --disable-db flag is set")
}
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
printer.Info("Connecting to database...")
// Connect to database
db, err := database.Connect(cfg)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer func() { _ = database.Close() }()
ctx := context.Background()
printer.Info("Running migrations...")
// Run migrations
if err := database.Migrate(ctx); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
printer.Success("Database migrations completed")
printer.Info("Database: %s", getDatabaseInfo(cfg, db))
return nil
}
// runDBList lists all database tables with row counts or records from a specific table
func runDBList(cmd *cobra.Command, args []string) error {
if disableDB {
return fmt.Errorf("database commands unavailable: --disable-db flag is set")
}
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
// Connect to database
_, err := database.Connect(cfg)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer func() { _ = database.Close() }()
ctx := context.Background()
// Ensure tables exist
if err := database.Migrate(ctx); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
// Handle --list-columns flag
if dbListColumns {
if dbTable == "" {
return fmt.Errorf("--list-columns requires --table/-t flag")
}
columns := database.GetAllTableColumns(dbTable)
if len(columns) == 0 {
return fmt.Errorf("unknown table or no columns found: %s", dbTable)
}
printer.Info("Available columns for table '%s':", dbTable)
for _, col := range columns {
fmt.Printf(" %s\n", col)
}
return nil
}
// JSON mode bypasses TUI entirely
if dbJSON {
if dbTable != "" {
return listTableRecordsJSON(ctx)
}
return listAllTablesJSON(ctx)
}
// No-TUI mode or specific table with flags: use plain text output
if dbNoTUI || dbTable != "" {
if dbTable != "" {
return listTableRecords(ctx, cfg, printer)
}
return listAllTables(ctx, cfg, printer)
}
// Default: use interactive TUI
return runDBListTUI(ctx)
}
// runDBListTUI starts the interactive database TUI
func runDBListTUI(ctx context.Context) error {
// Get all tables
tables, err := database.ListTables(ctx)
if err != nil {
return fmt.Errorf("failed to list tables: %w", err)
}
// Convert to terminal.TableInfo
tuiTables := make([]terminal.TableInfo, len(tables))
for i, t := range tables {
tuiTables[i] = terminal.TableInfo{
Name: t.Name,
RowCount: t.RowCount,
}
}
// Record fetcher wraps database.GetTableRecords
recordFetcher := func(ctx context.Context, tableName string, offset, limit int, filters map[string]string, search string) (*terminal.TableRecords, error) {
result, err := database.GetTableRecords(ctx, tableName, offset, limit, filters, search)
if err != nil {
return nil, err
}
return &terminal.TableRecords{
Table: result.Table,
TotalCount: result.TotalCount,
Offset: result.Offset,
Limit: result.Limit,
Records: result.Records,
}, nil
}
// Column fetcher wraps database.GetTableColumns (for display)
columnFetcher := func(tableName string) []string {
return database.GetTableColumns(tableName)
}
// All columns fetcher wraps database.GetAllTableColumns (for column selection)
allColumnsFetcher := func(tableName string) []string {
return database.GetAllTableColumns(tableName)
}
// Create and run TUI (pass dbLimit for page size)
tui := terminal.NewDBTUI(tuiTables, recordFetcher, columnFetcher, allColumnsFetcher, dbLimit)
return tui.Run()
}
// listAllTablesJSON outputs all tables as JSON
func listAllTablesJSON(ctx context.Context) error {
tables, err := database.ListTables(ctx)
if err != nil {
return fmt.Errorf("failed to list tables: %w", err)
}
jsonBytes, err := json.Marshal(tables)
if err != nil {
return fmt.Errorf("failed to format tables: %w", err)
}
fmt.Println(string(jsonBytes))
return nil
}
// listTableRecordsJSON outputs table records as JSON only
func listTableRecordsJSON(ctx context.Context) error {
// Validate limit
if dbLimit <= 0 {
dbLimit = 50
}
if dbLimit > 10000 {
dbLimit = 10000
}
if dbOffset < 0 {
dbOffset = 0
}
filters := parseWhereFilters(dbWhere)
records, err := database.GetTableRecords(ctx, dbTable, dbOffset, dbLimit, filters, dbSearch)
if err != nil {
return fmt.Errorf("failed to get records: %w", err)
}
jsonBytes, err := json.Marshal(records.Records)
if err != nil {
return fmt.Errorf("failed to format records: %w", err)
}
fmt.Println(string(jsonBytes))
return nil
}
// listAllTables lists all database tables with their row counts
func listAllTables(ctx context.Context, cfg *config.Config, printer *terminal.Printer) error {
tables, err := database.ListTables(ctx)
if err != nil {
return fmt.Errorf("failed to list tables: %w", err)
}
printer.Info("Database: %s", getDatabaseInfo(cfg, nil))
fmt.Println()
fmt.Printf("%-20s %s\n", "Table", "Rows")
fmt.Println("─────────────────────────────")
for _, t := range tables {
fmt.Printf("%-20s %d\n", t.Name, t.RowCount)
}
fmt.Println()
printer.Info("Use --table <name> to list records from a specific table")
return nil
}
// listTableRecords lists records from a specific table with pagination
func listTableRecords(ctx context.Context, cfg *config.Config, printer *terminal.Printer) error {
// Validate limit
if dbLimit <= 0 {
dbLimit = 50
}
if dbLimit > 10000 {
dbLimit = 10000
}
// Validate offset
if dbOffset < 0 {
dbOffset = 0
}
// Parse filters, columns, and exclude columns
filters := parseWhereFilters(dbWhere)
requestedColumns := parseColumns(dbColumns)
columns := getEffectiveColumns(dbTable, requestedColumns, dbAll)
excludeColumns := parseExcludeColumns(dbExcludeColumns)
// Determine if we should hide default columns (when no specific columns requested and not --all)
hideDefaultColumns := !dbAll && len(requestedColumns) == 0 && tableDefaultColumns[dbTable] == nil
records, err := database.GetTableRecords(ctx, dbTable, dbOffset, dbLimit, filters, dbSearch)
if err != nil {
return fmt.Errorf("failed to get records: %w", err)
}
// JSON-only output mode
if dbJSON {
jsonBytes, err := json.Marshal(records.Records)
if err != nil {
return fmt.Errorf("failed to format records: %w", err)
}
fmt.Println(string(jsonBytes))
return nil
}
// Calculate pagination info
startRecord := records.Offset + 1
endRecord := records.Offset + dbLimit
if endRecord > records.TotalCount {
endRecord = records.TotalCount
}
if records.TotalCount == 0 {
startRecord = 0
}
printer.Info("Table: %s", records.Table)
fmt.Printf("Showing records %d-%d of %d\n\n", startRecord, endRecord, records.TotalCount)
// Output as markdown table with glamour rendering
tableStr := formatAsMarkdownTable(records.Records, columns, dbWidth, hideDefaultColumns, excludeColumns)
renderer, err := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(0), // No word wrap for tables
)
if err == nil {
rendered, err := renderer.Render(tableStr)
if err == nil {
fmt.Print(rendered)
} else {
fmt.Println(tableStr)
}
} else {
fmt.Println(tableStr)
}
// Show pagination hints
if records.TotalCount > endRecord {
nextOffset := records.Offset + records.Limit
printer.Info("Next page: osmedeus db list -t %s --offset %d --limit %d", dbTable, nextOffset, dbLimit)
}
return nil
}
// parseWhereFilters parses --where flags into a map
func parseWhereFilters(whereFlags []string) map[string]string {
filters := make(map[string]string)
for _, w := range whereFlags {
parts := strings.SplitN(w, "=", 2)
if len(parts) == 2 {
filters[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
return filters
}
// parseColumns parses --columns flag into a slice
func parseColumns(columnsFlag string) []string {
if columnsFlag == "" {
return nil
}
cols := strings.Split(columnsFlag, ",")
for i := range cols {
cols[i] = strings.TrimSpace(cols[i])
}
return cols
}
// parseExcludeColumns parses --exclude-columns flag into a map for O(1) lookup
func parseExcludeColumns(excludeFlag string) map[string]bool {
excludeMap := make(map[string]bool)
if excludeFlag == "" {
return excludeMap
}
cols := strings.Split(excludeFlag, ",")
for _, col := range cols {
excludeMap[strings.TrimSpace(col)] = true
}
return excludeMap
}
// getEffectiveColumns determines which columns to display based on flags and table defaults
func getEffectiveColumns(tableName string, requestedColumns []string, showAll bool) []string {
// If user specified columns, use them as-is
if len(requestedColumns) > 0 {
return requestedColumns
}
// If --all flag, return nil (show all columns)
if showAll {
return nil
}
// Check for table-specific defaults
if defaults, ok := tableDefaultColumns[tableName]; ok {
return defaults
}
// Return nil to indicate "all except hidden"
return nil
}
// isHiddenColumn checks if a column should be hidden by default
func isHiddenColumn(col string) bool {
for _, hidden := range defaultHiddenColumns {
if col == hidden {
return true
}
}
return false
}
// formatAsMarkdownTable formats records as a markdown table
func formatAsMarkdownTable(records interface{}, columns []string, maxWidth int, hideDefaultColumns bool, excludeColumns map[string]bool) string {
// Convert records to []map[string]interface{}
jsonBytes, _ := json.Marshal(records)
var data []map[string]interface{}
if err := json.Unmarshal(jsonBytes, &data); err != nil {
return "No records found."
}
if len(data) == 0 {
return "No records found."
}
// Get headers (all keys or selected columns)
var headers []string
if len(columns) > 0 {
// Filter out excluded columns from specified columns
for _, col := range columns {
if !excludeColumns[col] {
headers = append(headers, col)
}
}
} else {
for key := range data[0] {
// Skip hidden columns if hideDefaultColumns is true
if hideDefaultColumns && isHiddenColumn(key) {
continue
}
// Skip excluded columns
if excludeColumns[key] {
continue
}
headers = append(headers, key)
}
sort.Strings(headers)
}
// Build markdown table
var sb strings.Builder
// Header row
sb.WriteString("| ")
sb.WriteString(strings.Join(headers, " | "))
sb.WriteString(" |\n")
// Separator row
sb.WriteString("|")
for range headers {
sb.WriteString(" --- |")
}
sb.WriteString("\n")
// Data rows
for _, row := range data {
sb.WriteString("| ")
for i, h := range headers {
val := formatTableValue(row[h], maxWidth)
if i > 0 {
sb.WriteString(" | ")
}
sb.WriteString(val)
}
sb.WriteString(" |\n")
}
return sb.String()
}
// formatTableValue converts a value to string for markdown display
func formatTableValue(v interface{}, maxWidth int) string {
if v == nil {
return ""
}
var s string
switch val := v.(type) {
case string:
// Escape pipe characters and newlines
s = strings.ReplaceAll(val, "|", "\\|")
s = strings.ReplaceAll(s, "\n", " ")
case map[string]interface{}, []interface{}:
// Compact JSON for complex types
b, _ := json.Marshal(val)
s = string(b)
default:
s = fmt.Sprintf("%v", val)
}
// Apply width limit
if maxWidth > 0 && len(s) > maxWidth {
if maxWidth > 3 {
return s[:maxWidth-3] + "..."
}
return s[:maxWidth]
}
return s
}
// getDatabaseInfo returns a human-readable database info string
func getDatabaseInfo(cfg *config.Config, db interface{}) string {
if cfg.IsPostgres() {
return fmt.Sprintf("PostgreSQL @ %s:%d/%s", cfg.Database.Host, cfg.Database.Port, cfg.Database.DBName)
}
return fmt.Sprintf("SQLite @ %s", cfg.GetDBPath())
}
// runIndexWorkflows indexes workflows from filesystem to database
func runIndexWorkflows(cmd *cobra.Command, args []string) error {
if disableDB {
return fmt.Errorf("database commands unavailable: --disable-db flag is set")
}
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
printer.Info("Connecting to database...")
// Connect to database
_, err := database.Connect(cfg)
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
defer func() { _ = database.Close() }()
ctx := context.Background()
// Ensure tables exist
if err := database.Migrate(ctx); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
printer.Info("Indexing workflows from: %s", cfg.WorkflowsPath)
if dbIndexForce {
printer.Info("Force mode: re-indexing all workflows")
}
// Index workflows
result, err := database.IndexWorkflowsFromFilesystem(ctx, cfg.WorkflowsPath, dbIndexForce)
if err != nil {
return fmt.Errorf("failed to index workflows: %w", err)
}
// Print results
printer.Success("Workflow indexing completed")
fmt.Println()
fmt.Printf(" Added: %d\n", result.Added)
fmt.Printf(" Updated: %d\n", result.Updated)
fmt.Printf(" Removed: %d\n", result.Removed)
if len(result.Errors) > 0 {
fmt.Println()
printer.Warning("Errors encountered:")
for _, e := range result.Errors {
printer.Bullet(e)
}
}
// Show total count
count, err := database.GetWorkflowCount(ctx)
if err == nil {
fmt.Println()
printer.Info("Total workflows indexed: %d", count)
}
return nil
}
+239
View File
@@ -0,0 +1,239 @@
package cli
import (
"fmt"
"io"
"os"
"strings"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/database"
"github.com/j3ssie/osmedeus/v5/internal/functions"
"github.com/j3ssie/osmedeus/v5/internal/template"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/spf13/cobra"
)
var (
evalScript string
evalTarget string
evalParams []string
evalStdin bool
evalFunctionName string
funcSearchFilter string
funcColumnWidth int
funcShowExample bool
)
// functionCmd is the parent command for function operations
var functionCmd = &cobra.Command{
Use: "function",
Aliases: []string{"func"},
Short: "Execute and test utility functions",
Long: UsageFunction(),
}
// functionEvalCmd evaluates a script with template rendering and function execution
var functionEvalCmd = &cobra.Command{
Use: "eval",
Aliases: []string{"e"},
Short: "Evaluate a script with template rendering and function execution",
Long: UsageFunctionEval(),
RunE: runFunctionEval,
}
// functionListCmd lists all available functions
var functionListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List all available utility functions",
RunE: runFunctionList,
}
func init() {
functionEvalCmd.Flags().StringVarP(&evalScript, "eval", "e", "", "script to evaluate")
functionEvalCmd.Flags().StringVarP(&evalTarget, "target", "t", "", "target value for {{target}} variable")
functionEvalCmd.Flags().StringArrayVar(&evalParams, "params", nil, "additional parameters (key=value format)")
functionEvalCmd.Flags().BoolVar(&evalStdin, "stdin", false, "read script from stdin")
functionEvalCmd.Flags().StringVarP(&evalFunctionName, "function", "f", "", "function name to call (remaining args become function arguments)")
functionListCmd.Flags().StringVarP(&funcSearchFilter, "search", "s", "", "filter functions by name or description")
functionListCmd.Flags().IntVar(&funcColumnWidth, "width", 0, "max column width (wraps lines instead of truncating)")
functionListCmd.Flags().BoolVar(&funcShowExample, "example", false, "show example usage below each function description")
functionCmd.AddCommand(functionEvalCmd)
functionCmd.AddCommand(functionListCmd)
}
func runFunctionEval(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
// Connect to database for db_* functions (skip if --disable-db is set)
if !disableDB {
cfg := config.Get()
if cfg != nil {
// Try to connect to database - don't fail if DB not available
if _, dbErr := database.Connect(cfg); dbErr != nil {
if verbose {
printer.Info("Database connection warning: %s", dbErr)
}
}
}
}
// Determine script source: -f flag > positional arg > -e flag > stdin
var script string
// Handle -f/--function flag: build script from function name + positional args
if evalFunctionName != "" {
var quotedArgs []string
for _, arg := range args {
quotedArgs = append(quotedArgs, fmt.Sprintf("%q", arg))
}
script = fmt.Sprintf("%s(%s)", evalFunctionName, strings.Join(quotedArgs, ", "))
} else if len(args) > 0 && args[0] != "-" {
if len(args) > 1 {
// Multiple args: treat first as function name, rest as arguments
// e.g., "func_name arg1 arg2" → "func_name("arg1", "arg2")"
var quotedArgs []string
for _, arg := range args[1:] {
quotedArgs = append(quotedArgs, fmt.Sprintf("%q", arg))
}
script = fmt.Sprintf("%s(%s)", args[0], strings.Join(quotedArgs, ", "))
} else {
// Single arg: use as-is (could be full expression or function name with no args)
script = args[0]
}
} else if evalScript != "" {
// Script provided via -e flag
script = evalScript
} else if evalStdin || (len(args) > 0 && args[0] == "-") {
// Read script from stdin
data, err := io.ReadAll(os.Stdin)
if err != nil {
printer.Error("Failed to read from stdin: %s", err)
return fmt.Errorf("failed to read from stdin: %w", err)
}
script = strings.TrimSpace(string(data))
}
if script == "" {
return fmt.Errorf("no script provided: use positional argument, -e flag, or --stdin")
}
// Use the resolved script
evalScript = script
// 1. Build context with target and params
ctx := make(map[string]interface{})
if evalTarget != "" {
ctx["target"] = evalTarget
}
for _, p := range evalParams {
parts := strings.SplitN(p, "=", 2)
if len(parts) == 2 {
ctx[parts[0]] = parts[1]
}
}
// 2. Render template variables ({{target}}, etc.)
templateEngine := template.NewEngine()
renderedScript, err := templateEngine.Render(evalScript, ctx)
if err != nil {
printer.Error("Template rendering failed: %s", err)
return fmt.Errorf("template rendering failed: %w", err)
}
// Show rendered script if different from original (verbose mode)
if verbose && renderedScript != evalScript {
printer.Info("Rendered script: %s", renderedScript)
}
// 3. Execute as JavaScript using Otto runtime
registry := functions.NewRegistry()
result, err := registry.Execute(renderedScript, ctx)
if err != nil {
printer.Error("Execution failed: %s", err)
return fmt.Errorf("execution failed: %w", err)
}
// 4. Print result
if result != nil {
fmt.Println(result)
}
return nil
}
func runFunctionList(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
printer.Section("Available Utility Functions")
fmt.Println()
// Use positional arg as search if --search not provided
if funcSearchFilter == "" && len(args) > 0 {
funcSearchFilter = args[0]
}
// Get function registry and category order
registry := functions.FunctionRegistry()
categories := functions.CategoryOrder()
// Build rows for all functions
var rows [][]string
searchLower := strings.ToLower(funcSearchFilter)
for _, cat := range categories {
if funcs, ok := registry[cat.Key]; ok {
for _, fn := range funcs {
// Apply search filter if specified (matches name, description, or category)
if funcSearchFilter != "" {
nameLower := strings.ToLower(fn.Name)
descLower := strings.ToLower(fn.Description)
catLower := strings.ToLower(cat.Title)
if !strings.Contains(nameLower, searchLower) &&
!strings.Contains(descLower, searchLower) &&
!strings.Contains(catLower, searchLower) {
continue
}
}
// Build description, optionally with example
desc := fn.Description
if funcShowExample && fn.Example != "" {
desc = fn.Description + "\n" + terminal.Gray("e.g. "+fn.Example)
}
rows = append(rows, []string{
terminal.Yellow(cat.ShortTitle),
terminal.Cyan(fn.Signature),
desc,
terminal.Magenta(fn.ReturnType),
})
}
}
}
if len(rows) == 0 {
if funcSearchFilter != "" {
printer.Info("No functions matching '%s'", funcSearchFilter)
} else {
printer.Info("No functions available")
}
return nil
}
if funcSearchFilter != "" {
printer.Info("Found %d function(s) matching '%s':", len(rows), funcSearchFilter)
fmt.Println()
}
headers := []string{"Category", "Function", "Description", "Returns"}
if funcColumnWidth > 0 {
printMarkdownTableWithWidth(headers, rows, funcColumnWidth)
} else {
printMarkdownTable(headers, rows)
}
return nil
}
+354
View File
@@ -0,0 +1,354 @@
package cli
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/parser"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/j3ssie/osmedeus/v5/public"
"github.com/spf13/cobra"
)
var healthCmd = &cobra.Command{
Use: "health",
Short: "Check and fix environment health (alias for 'osmedeus install validate')",
Long: UsageHealth(),
RunE: runHealthWithSample,
}
func runHealthWithSample(cmd *cobra.Command, args []string) error {
return runHealth(cmd, args)
}
func runHealth(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
cfg := config.Get()
// Check if first-time setup is needed and run it
if isFirstTimeSetupNeeded(cfg.BaseFolder) {
if err := runFirstTimeSetup(cfg.BaseFolder, cfg); err != nil {
printer.Warning("First-time setup had issues: %s", err)
}
// Reload config after setup
if reloaded, err := config.Load(cfg.BaseFolder); err == nil {
cfg = reloaded
config.Set(cfg)
}
}
printer.Newline()
printer.Println("%s Osmedeus Environment Health Check %s",
terminal.Yellow(terminal.SymbolMenu),
terminal.Cyan(core.VERSION))
hasErrors := false
// 1. Check/create folders
hasErrors = checkFolders(printer, cfg) || hasErrors
// 2. Check config files
hasErrors = checkConfigFiles(printer, cfg) || hasErrors
// 3. Check workflows
hasErrors = checkWorkflows(printer, cfg) || hasErrors
// Summary
fmt.Println()
if hasErrors {
printer.Warning("Some issues were found. Review the output above.")
} else {
printer.Success("All checks passed!")
}
printer.Newline()
printer.Println("%s %s %s", terminal.Yellow(terminal.SymbolLightning), terminal.BoldCyan("Tip:"), terminal.Gray("See the full CLI documentation below for more details"))
printer.Println(" %s", terminal.Green("https://docs.osmedeus.org/getting-started/cli"))
return nil
}
func checkFolders(printer *terminal.Printer, cfg *config.Config) bool {
printer.Section("Environments Folders")
if _, err := os.Stat(cfg.BaseFolder); os.IsNotExist(err) {
err := copyEmbeddedAssets(cfg.BaseFolder)
if err != nil {
printer.Error(" Base folder: failed to create %s - %v", terminal.White(cfg.BaseFolder), err)
return true
}
printer.Success(" Base folder: created %s", terminal.White(cfg.BaseFolder))
} else {
printer.Success(" Base folder: %s", terminal.White(cfg.BaseFolder))
}
folders := []struct {
name string
path string
}{
{"Workspaces", cfg.WorkspacesPath},
{"Workflows", cfg.WorkflowsPath},
{"Binaries", cfg.BinariesPath},
{"Data", cfg.DataPath},
{"Markdown Report Templates", cfg.MarkdownReportTemplatesPath},
{"External Agent Configs", cfg.ExternalAgentConfigsPath},
}
hasErrors := false
for _, f := range folders {
if f.path == "" {
if f.name == "Binaries" {
printer.Error(" %s: path not configured", f.name)
hasErrors = true
} else {
printer.Info(" %s: not configured", f.name)
}
continue
}
if _, err := os.Stat(f.path); os.IsNotExist(err) {
// Create folder
if err := os.MkdirAll(f.path, 0755); err != nil {
printer.Error(" %s: failed to create %s - %v", f.name, terminal.White(f.path), err)
hasErrors = true
} else {
printer.Success(" %s: created %s", f.name, terminal.White(f.path))
}
} else {
printer.Success(" %s: %s", f.name, terminal.White(f.path))
// Check if binaries folder is empty (ignoring hidden files like .gitkeep)
if f.name == "Binaries" {
entries, err := os.ReadDir(f.path)
if err == nil {
binaryCount := 0
for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), ".") {
binaryCount++
}
}
if binaryCount == 0 {
printer.Error(" %s No binaries detected in %s",
terminal.Red("✗"),
terminal.White(f.path))
printer.Println(" %s Run %s to fetch required binaries",
terminal.Yellow("→"),
terminal.Cyan("osmedeus install binary --all"))
hasErrors = true
}
}
}
}
}
binariesFolder := cfg.BinariesPath
if binariesFolder == "" {
binariesFolder = filepath.Join(cfg.BaseFolder, "binaries")
}
// Use the shared helper to setup PATH (updates shell config + current process)
ensureBinariesPathInEnv(printer, binariesFolder, true)
return hasErrors
}
func checkConfigFiles(printer *terminal.Printer, cfg *config.Config) bool {
printer.Section("Configuration Files")
hasErrors := false
// Check osm-settings.yaml
settingsPath := filepath.Join(cfg.BaseFolder, "osm-settings.yaml")
if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
if err := config.EnsureConfigExists(cfg.BaseFolder); err != nil {
printer.Error(" osm-settings.yaml: failed to create - %v", err)
hasErrors = true
} else {
printer.Success(" osm-settings.yaml: created default config")
}
} else {
// Validate existing config
if err := cfg.Validate(); err != nil {
printer.Error(" osm-settings.yaml: validation failed - %v", err)
hasErrors = true
} else {
printer.Success(" osm-settings.yaml: valid")
}
}
// Check global_vars configuration
if len(cfg.GlobalVars) > 0 {
printer.Success(" global_vars: %s variable(s) configured", terminal.White(fmt.Sprintf("%d", len(cfg.GlobalVars))))
} else {
printer.Info(" global_vars: no variables configured")
}
// Check notification configuration
if cfg.IsNotificationConfigured() {
printer.Success(" notification: %s enabled", terminal.White(cfg.Notification.Provider))
} else {
printer.Info(" notification: not configured")
}
return hasErrors
}
func checkWorkflows(printer *terminal.Printer, cfg *config.Config) bool {
printer.Section("Workflows")
if cfg.WorkflowsPath == "" {
printer.Error(" Workflows path not configured")
return true
}
// Check if workflows directory exists
if _, err := os.Stat(cfg.WorkflowsPath); os.IsNotExist(err) {
printer.Warning(" No workflows found in %s", terminal.White(cfg.WorkflowsPath))
return false
}
workflowFiles, err := findWorkflowYAMLFiles(cfg.WorkflowsPath)
if err != nil {
printer.Error(" Failed to scan workflows folder: %v", err)
return true
}
if len(workflowFiles) == 0 {
printer.Warning(" No workflows found in %s", terminal.White(cfg.WorkflowsPath))
return false
}
p := parser.NewParser()
validCount := 0
invalidCount := 0
for _, filePath := range workflowFiles {
relPath, _ := filepath.Rel(cfg.WorkflowsPath, filePath)
relPath = filepath.Clean(relPath)
wf, err := p.Parse(filePath)
if err != nil {
printer.Error(" [INVALID] %s: %v", terminal.White(relPath), err)
invalidCount++
continue
}
if err := p.Validate(wf); err != nil {
printer.Error(" [INVALID] %s (%s): %v", terminal.White(relPath), terminal.White(wf.Name), err)
invalidCount++
continue
}
printer.Success(" [VALID] %s (%s) - %s", terminal.White(wf.Name), wf.Kind, terminal.Gray(relPath))
validCount++
}
fmt.Println()
printer.Info(" Total: %s valid, %s invalid", terminal.Green(fmt.Sprintf("%d", validCount)), terminal.Red(fmt.Sprintf("%d", invalidCount)))
return invalidCount > 0
}
func findWorkflowYAMLFiles(root string) ([]string, error) {
root = filepath.Clean(root)
var out []string
queue := []string{root}
seen := make(map[string]struct{})
for len(queue) > 0 {
dir := queue[len(queue)-1]
queue = queue[:len(queue)-1]
realDir := dir
if eval, err := filepath.EvalSymlinks(dir); err == nil {
realDir = eval
}
if _, ok := seen[realDir]; ok {
continue
}
seen[realDir] = struct{}{}
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
}
for _, entry := range entries {
name := entry.Name()
if name == "." || name == ".." {
continue
}
fullPath := filepath.Join(dir, name)
if entry.IsDir() {
queue = append(queue, fullPath)
continue
}
if entry.Type()&os.ModeSymlink != 0 {
info, err := os.Stat(fullPath)
if err == nil && info.IsDir() {
queue = append(queue, fullPath)
continue
}
}
lower := strings.ToLower(name)
if strings.HasSuffix(lower, ".yaml") || strings.HasSuffix(lower, ".yml") {
out = append(out, fullPath)
}
}
}
sort.Strings(out)
return out, nil
}
func copyEmbeddedAssets(dest string) error {
srcFS := public.EmbedFS
srcRoot := "examples/osmedeus-base.example"
return fs.WalkDir(srcFS, srcRoot, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(srcRoot, path)
if err != nil {
return err
}
if relPath == "." {
return os.MkdirAll(dest, 0755)
}
destPath := filepath.Join(dest, relPath)
if d.IsDir() {
return os.MkdirAll(destPath, 0755)
}
srcFile, err := srcFS.Open(path)
if err != nil {
return err
}
defer func() { _ = srcFile.Close() }()
destFile, err := os.Create(destPath)
if err != nil {
return err
}
defer func() { _ = destFile.Close() }()
_, err = io.Copy(destFile, srcFile)
return err
})
}
+1723
View File
File diff suppressed because it is too large Load Diff
+579
View File
@@ -0,0 +1,579 @@
package cli
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/installer"
"github.com/j3ssie/osmedeus/v5/internal/logger"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var (
settingsFile string
baseFolder string
workflowFolder string
verbose bool
debug bool
silent bool
logFile string
logFileTmp bool
showUsageExamples bool
showSpinner bool
disableLogging bool
disableColor bool
disableNotification bool
fullUsageExample bool
disableDB bool
ciOutputFormat bool
skipAutoSetup bool
// Build info - set via SetBuildInfo from main.go
buildTime = "unknown"
commitHash = "unknown"
)
// SetBuildInfo sets the build time and commit hash for version display
// This must be called before Execute() to ensure version output is correct
func SetBuildInfo(bt, ch string) {
buildTime = bt
commitHash = ch
// Set version template here (after build info is set) instead of in init()
// This ensures the template uses actual values instead of "unknown"
rootCmd.SetVersionTemplate(fmt.Sprintf(`%s - %s
Version: {{.Version}}
Build: %s
Commit: %s
Author: %s
Docs: %s
`, core.BINARY, core.DESC, buildTime, commitHash, core.AUTHOR, core.DOCS))
}
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "osmedeus",
Short: "Osmedeus - Workflow Engine for Automated Reconnaissance",
Version: core.VERSION,
Long: UsageRoot(),
Run: func(cmd *cobra.Command, args []string) {
if showUsageExamples {
fmt.Print(terminal.Banner())
fmt.Println(UsageAllExamples())
return
}
if fullUsageExample {
fmt.Print(terminal.Banner())
showInPager(UsageFullExample())
return
}
// Default: show help
_ = cmd.Help()
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// Handle CI output format - disable colors and decorations early
if ciOutputFormat {
disableColor = true
silent = true // Auto-enable silent mode in CI
terminal.SetColorEnabled(false)
terminal.SetCIMode(true)
}
// Debug mode enables verbose automatically
if debug {
verbose = true
}
// Determine log file path
actualLogFile := logFile
if logFileTmp && actualLogFile == "" {
// Generate temporary log file with timestamp
timestamp := time.Now().Format("20060102-150405")
actualLogFile = fmt.Sprintf("osmedeus-log-%s.log", timestamp)
}
// Initialize logger
logCfg := logger.DefaultConfig()
if silent || disableLogging {
logCfg.Level = "error" // Only show errors in silent mode
logCfg.Silent = true
} else if debug {
// Only debug mode sets DEBUG level, verbose mode shows step output instead
logCfg.Level = "debug"
logCfg.Development = true
logCfg.Verbose = true // Show caller/source file in debug mode
}
// Note: verbose mode now shows actual step output instead of DEBUG logs
if actualLogFile != "" {
logCfg.LogFile = actualLogFile
}
if err := logger.Init(logCfg); err != nil {
return fmt.Errorf("failed to initialize logger: %w", err)
}
if silent {
_ = os.Setenv("OSMEDEUS_SILENT", "1")
} else {
_ = os.Unsetenv("OSMEDEUS_SILENT")
}
// Disable colors if requested
if disableColor {
terminal.SetColorEnabled(false)
}
// Log the log file path if set
if actualLogFile != "" {
logger.Get().Debug("Logging to file", zap.String("log_file", actualLogFile))
}
// Load configuration
if baseFolder == "" {
homeDir, _ := os.UserHomeDir()
baseFolder = homeDir + "/osmedeus-base"
}
logger.Get().Debug("Using base folder", zap.String("base_folder", baseFolder))
// Auto-generate config files if they don't exist
if err := config.EnsureConfigExists(baseFolder); err != nil {
logger.Get().Warn("Failed to create default config", zap.Error(err))
}
// Load configuration from custom file or base folder
var cfg *config.Config
var err error
if settingsFile != "" {
// Load from custom settings file
logger.Get().Debug("Loading configuration from custom file",
zap.String("settings_file", settingsFile),
)
cfg, err = config.LoadFromFile(settingsFile)
if err != nil {
return fmt.Errorf("failed to load config from %s: %w", settingsFile, err)
}
// Set base folder if not specified in config
if cfg.BaseFolder == "" {
cfg.BaseFolder = baseFolder
}
} else {
logger.Get().Debug("Loading configuration",
zap.String("base_folder", baseFolder),
zap.String("config_file", baseFolder+"/osm-settings.yaml"),
)
cfg, err = config.Load(baseFolder)
if err != nil {
// Use default config if loading fails
logger.Get().Debug("Using default config", zap.Error(err))
cfg = config.DefaultConfig()
cfg.BaseFolder = baseFolder
cfg.ResolvePaths() // Recalculate all paths with new base folder
}
}
logger.Get().Debug("Configuration loaded",
zap.String("base_folder", cfg.BaseFolder),
zap.String("workflows_path", cfg.WorkflowsPath),
zap.String("workspaces_path", cfg.WorkspacesPath),
zap.String("db_path", cfg.GetDBPath()),
)
// Override workflow folder if specified
if workflowFolder != "" {
cfg.WorkflowsPath = workflowFolder
logger.Get().Info("Using custom workflow folder", zap.String("path", workflowFolder))
}
// Export global vars to environment
cfg.ExportGlobalVarsToEnv()
logger.Get().Debug("Exported global vars to environment",
zap.Int("var_count", len(cfg.GlobalVars)),
)
// Disable notifications if flag is set
if disableNotification {
cfg.Notification.Enabled = false
logger.Get().Debug("Notifications disabled via CLI flag")
}
config.Set(cfg)
// Check for first-time setup (after config is loaded)
if !shouldSkipAutoSetup(cmd) && isFirstTimeSetupNeeded(baseFolder) {
if err := runFirstTimeSetup(baseFolder, cfg); err != nil {
logger.Get().Warn("First-time setup had issues", zap.Error(err))
}
// Reload config after setup
if reloaded, err := config.Load(baseFolder); err == nil {
cfg = reloaded
config.Set(cfg)
}
}
// Show warning if database is disabled (skip warning in CI mode)
if disableDB && !ciOutputFormat {
printer := terminal.NewPrinter()
printer.Warning("Database disabled via --disable-db flag. The following features are unavailable:")
fmt.Println(" - Database queries (db_select, db_select_*, etc.)")
fmt.Println(" - Asset/vulnerability tracking")
fmt.Println(" - Workspace statistics")
fmt.Println(" - The 'db' subcommand")
fmt.Println(" Use this mode for lightweight scanning without persistence.")
fmt.Println()
}
return nil
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "%s %s\n", terminal.Red("Error:"), err)
os.Exit(1)
}
}
func init() {
rootCmd.PersistentFlags().StringVar(&settingsFile, "settings-file", "", "settings file path (default is $HOME/osmedeus-base/osm-settings.yaml)")
rootCmd.PersistentFlags().StringVarP(&baseFolder, "base-folder", "b", "", "base folder containing workflows and settings (default is $HOME/osmedeus-base/)")
rootCmd.PersistentFlags().StringVarP(&workflowFolder, "workflow-folder", "F", "", "custom workflow folder (default is $HOME/osmedeus-base/workflows/)")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output")
rootCmd.PersistentFlags().BoolVar(&debug, "debug", false, "enable debug mode (verbose + debug logging)")
rootCmd.PersistentFlags().BoolVarP(&silent, "silent", "q", false, "silent mode - suppress all output except errors")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", "", "path to log file (logs to both console and file)")
rootCmd.PersistentFlags().BoolVar(&logFileTmp, "log-file-tmp", false, "create temporary log file osmedeus-log-<timestamp>.log")
rootCmd.PersistentFlags().BoolVarP(&showUsageExamples, "usage-example", "H", false, "show comprehensive usage examples for all commands")
rootCmd.PersistentFlags().BoolVar(&showSpinner, "spinner", false, "show spinner animations during execution")
rootCmd.PersistentFlags().BoolVar(&disableLogging, "disable-logging", false, "disable all logging output")
rootCmd.PersistentFlags().BoolVar(&disableColor, "disable-color", false, "disable colored output")
rootCmd.PersistentFlags().BoolVar(&disableNotification, "disable-notification", false, "disable all notifications")
rootCmd.PersistentFlags().BoolVar(&fullUsageExample, "full-usage-example", false, "show full usage with all flags in pager mode")
rootCmd.PersistentFlags().BoolVar(&disableDB, "disable-db", false, "disable database connection (warning: some features unavailable)")
rootCmd.PersistentFlags().BoolVar(&ciOutputFormat, "ci-output-format", false, "output results in JSON format for CI pipelines")
rootCmd.PersistentFlags().BoolVar(&skipAutoSetup, "skip-auto-setup", false, "skip automatic first-time setup")
// Suppress usage display and default error output (we handle errors in Execute())
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
// Note: Version template is set in SetBuildInfo() to use actual build values
// Set custom help function to show banner before help
defaultHelpFunc := rootCmd.HelpFunc()
rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
fmt.Print(terminal.Banner())
defaultHelpFunc(cmd, args)
})
// Add subcommands
rootCmd.AddCommand(runCmd)
rootCmd.AddCommand(scanCmd) // Alias for runCmd (backward compatibility)
rootCmd.AddCommand(serveCmd)
rootCmd.AddCommand(workflowCmd)
rootCmd.AddCommand(functionCmd)
rootCmd.AddCommand(workerCmd)
rootCmd.AddCommand(healthCmd)
rootCmd.AddCommand(configCmd)
rootCmd.AddCommand(dbCmd)
rootCmd.AddCommand(installCmd)
rootCmd.AddCommand(snapshotCmd)
rootCmd.AddCommand(versionCmd)
rootCmd.AddCommand(updateCmd)
}
// versionCmd shows version information
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s - %s\n", core.BINARY, core.DESC)
fmt.Printf("Version: %s\n", core.VERSION)
fmt.Printf("Build: %s\n", buildTime)
fmt.Printf("Commit: %s\n", commitHash)
fmt.Printf("Author: %s\n", core.AUTHOR)
fmt.Printf("Docs: %s\n", core.DOCS)
},
}
// showInPager displays content using a pager (less/more) if available
func showInPager(content string) {
// Try less first, fall back to more, fall back to direct output
pagers := []string{"less", "more"}
for _, pager := range pagers {
if path, err := exec.LookPath(pager); err == nil {
cmd := exec.Command(path, "-R") // -R for color support
cmd.Stdin = strings.NewReader(content)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err == nil {
return
}
}
}
// Fallback: direct output
fmt.Print(content)
}
// shouldSkipAutoSetup returns true if auto-setup should be skipped for this command
func shouldSkipAutoSetup(cmd *cobra.Command) bool {
if skipAutoSetup {
return true
}
// Skip for commands that handle their own setup or don't need it
skipCommands := map[string]bool{
"install": true,
"health": true,
"version": true,
"help": true,
"update": true,
"completion": true,
}
// Check command and all parent commands
for c := cmd; c != nil; c = c.Parent() {
if skipCommands[c.Name()] {
return true
}
}
return false
}
// isFirstTimeSetupNeeded checks if base folder needs initialization
// Returns false if $HOME/.osmedeus/initialized marker or database file exists
func isFirstTimeSetupNeeded(baseFolder string) bool {
if baseFolder == "" {
return false
}
// Check for initialization marker file in $HOME/.osmedeus/
homeDir, _ := os.UserHomeDir()
markerFile := filepath.Join(homeDir, ".osmedeus", "initialized")
if _, err := os.Stat(markerFile); err == nil {
return false // Marker exists, setup already done
}
// Check for database file (also indicates setup was done)
dbFile := filepath.Join(baseFolder, "database-osm.sqlite")
if _, err := os.Stat(dbFile); err == nil {
return false // Database exists, setup already done
}
return true // Neither exists, first-time setup needed
}
// runFirstTimeSetup performs automatic first-time setup
func runFirstTimeSetup(baseFolder string, cfg *config.Config) error {
printer := terminal.NewPrinter()
// Show welcome banner
printer.Newline()
printer.Println("%s %s", terminal.Yellow(terminal.SymbolStar), terminal.BoldCyan("Welcome to Osmedeus!"))
printer.Println(" %s", terminal.HiMagenta("First-time setup detected."))
printer.Newline()
// Step 1: Initialize the environment
printer.Println("%s %s", terminal.BoldBlue(terminal.SymbolLightning), terminal.HiBlue("Initializing environment..."))
printer.Println(" %s Base folder: %s", terminal.SymbolBullet, terminal.Cyan(baseFolder))
// Check for custom preset URL from environment
presetURL := os.Getenv("OSM_PRESET_URL")
if presetURL == "" {
presetURL = core.DEFAULT_BASE_REPO
}
printer.Println(" %s Preset URL: %s", terminal.SymbolBullet, terminal.Cyan(presetURL))
// Check for custom workflow URL from environment
workflowURL := os.Getenv("OSM_WORKFLOW_URL")
if workflowURL != "" {
printer.Println(" %s Workflow URL: %s", terminal.SymbolBullet, terminal.Cyan(workflowURL))
}
printer.Newline()
// Step 2: Install preset base folder (workflows, etc.)
printer.Println("%s %s", terminal.BoldMagenta(terminal.SymbolLightning), terminal.HiMagenta("Installing preset workflows..."))
printer.Println(" %s Downloading from: %s", terminal.SymbolBullet, terminal.Cyan(presetURL))
inst := installer.NewInstaller(
baseFolder,
filepath.Join(baseFolder, "workflows"),
filepath.Join(baseFolder, "external-binaries"),
nil,
)
if err := inst.InstallBase(presetURL); err != nil {
printer.Warning("Failed to install preset workflows: %s", err)
printer.Println(" %s", terminal.Gray("You can manually run: osmedeus install validate --preset"))
return err
}
printer.Success("Preset workflows installed to: %s", terminal.Cyan(filepath.Join(baseFolder, "workflows")))
printer.Newline()
// Step 3: Install workflows from separate URL if specified
if workflowURL != "" {
printer.Println("%s %s", terminal.BoldBlue(terminal.SymbolLightning), terminal.HiBlue("Installing workflows from OSM_WORKFLOW_URL..."))
printer.Println(" %s Downloading from: %s", terminal.SymbolBullet, terminal.Cyan(workflowURL))
if err := inst.InstallWorkflow(workflowURL); err != nil {
printer.Warning("Failed to install workflows from OSM_WORKFLOW_URL: %s", err)
} else {
printer.Success("Workflows installed from: %s", terminal.Cyan(workflowURL))
}
printer.Newline()
}
// Step 4: Reload config and load workflows
printer.Println("%s %s", terminal.BoldMagenta(terminal.SymbolLightning), terminal.HiMagenta("Loading workflows..."))
// Ensure config exists after InstallBase (which may have removed the base folder)
if err := config.EnsureConfigExists(baseFolder); err != nil {
printer.Warning("Failed to create config: %s", err)
}
reloaded, err := config.Load(baseFolder)
if err == nil {
config.Set(reloaded)
cfg = reloaded
printer.Success("Configuration loaded from: %s", terminal.Cyan(filepath.Join(baseFolder, "osm-settings.yaml")))
} else {
printer.Warning("Failed to reload config: %s", err)
}
printer.Newline()
// Step 5: Install all binaries
// Check for registry URL override
registryURL := os.Getenv("OSM_REGISTRY_URL")
registryDisplay := "the default registry"
if registryURL != "" {
registryDisplay = registryURL
}
printer.Println("%s %s", terminal.BoldBlue(terminal.SymbolLightning),
terminal.HiBlue(fmt.Sprintf("Installing security binaries from %s", terminal.Cyan(registryDisplay))))
printer.Println(" %s This may take a few minutes depending on your network speed", terminal.SymbolBullet)
printer.Newline()
// Show spinner while loading registry
loadingSpinner := terminal.LoadingSpinner("Loading binary registry")
loadingSpinner.Start()
registry, err := installer.LoadRegistry(registryURL, nil)
loadingSpinner.Stop()
if err != nil {
printer.Warning("Failed to load binary registry: %s", err)
printer.Println(" %s", terminal.Gray("You can manually run: osmedeus install binary --all"))
return err
}
binariesFolder := cfg.BinariesPath
if binariesFolder == "" {
binariesFolder = filepath.Join(baseFolder, "external-binaries")
}
// Create binaries folder if it doesn't exist
if err := os.MkdirAll(binariesFolder, 0755); err != nil {
printer.Warning("Failed to create binaries folder: %s", err)
}
// Count binaries to install
var toInstall []string
for name, entry := range registry {
isOptional := false
for _, tag := range entry.Tags {
if tag == "optional" {
isOptional = true
break
}
}
if !isOptional && !installer.IsBinaryInPath(name) {
toInstall = append(toInstall, name)
}
}
if len(toInstall) > 0 {
printer.Println(" %s Installing %s binaries to: %s", terminal.SymbolBullet, terminal.Green(fmt.Sprintf("%d", len(toInstall))), terminal.Cyan(binariesFolder))
printer.Newline()
}
// Sort binary names for consistent display
sort.Strings(toInstall)
// Set silent mode for binary installation to reduce noise
_ = os.Setenv("OSMEDEUS_SILENT", "1")
defer func() { _ = os.Unsetenv("OSMEDEUS_SILENT") }()
// Suppress logger output during binary installation
logCfg := logger.DefaultConfig()
logCfg.Silent = true
_ = logger.Init(logCfg)
defer func() {
// Restore normal logging after installation
logCfg.Silent = false
_ = logger.Init(logCfg)
}()
// Install binaries in parallel with multi-row spinner display
var failed []string
if len(toInstall) > 0 {
failed = installBinariesParallel(toInstall, registry, binariesFolder, nil, printer, false)
}
// Count results
installedCount := len(toInstall) - len(failed)
failedCount := len(failed)
// Count skipped (already in PATH) - these weren't in toInstall
skippedCount := 0
for name, entry := range registry {
isOptional := false
for _, tag := range entry.Tags {
if tag == "optional" {
isOptional = true
break
}
}
if !isOptional && installer.IsBinaryInPath(name) {
skippedCount++
}
}
printer.Newline()
// Show summary
printer.Success("Installed %s binaries (%s skipped, %s failed)",
terminal.Green(fmt.Sprintf("%d", installedCount)),
terminal.Yellow(fmt.Sprintf("%d", skippedCount)),
terminal.Red(fmt.Sprintf("%d", failedCount)))
// Ensure binaries path is in environment
ensureBinariesPathInEnv(printer, binariesFolder, false)
// Create initialization marker file in $HOME/.osmedeus/
homeDir, _ := os.UserHomeDir()
osmDir := filepath.Join(homeDir, ".osmedeus")
if err := os.MkdirAll(osmDir, 0755); err != nil {
printer.Warning("Failed to create osmedeus config directory: %s", err)
}
markerFile := filepath.Join(osmDir, "initialized")
if err := os.WriteFile(markerFile, []byte("initialized\n"), 0644); err != nil {
printer.Warning("Failed to create initialization marker: %s", err)
}
// Print completion message
printer.Newline()
printer.Println("%s %s", terminal.Green(terminal.SymbolSuccess), terminal.BoldGreen("First-time setup complete!"))
printer.Newline()
// Print next steps hint
printer.Println("%s %s", terminal.BoldMagenta(terminal.SymbolLightning), terminal.HiMagenta("Next Steps:"))
printer.Println(" %s Run a scan: %s", terminal.SymbolBullet, terminal.Cyan("osmedeus run -f basic-recon -t example.com"))
printer.Println(" %s Check health: %s", terminal.SymbolBullet, terminal.Cyan("osmedeus health"))
printer.Newline()
return nil
}
+1272
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
package cli
import (
"fmt"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/spf13/cobra"
)
// scanCmd is an alias for runCmd (backward compatibility)
var scanCmd = &cobra.Command{
Use: "scan",
Aliases: []string{"execute"},
Short: "Execute a workflow (alias for 'run')",
Long: UsageRun(),
RunE: func(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
tip := "Tip: 'osmedeus scan' and 'osmedeus execute' are aliases for 'osmedeus run'"
if terminal.IsCIMode() {
printer.Info("%s", tip)
} else {
printer.Info("%s", terminal.Gray(tip))
}
fmt.Println()
return runRun(cmd, args)
},
}
func init() {
// Copy all flags from runCmd to scanCmd for backward compatibility
scanCmd.Flags().StringVarP(&flowName, "flow", "f", "", "flow workflow name to execute")
scanCmd.Flags().StringArrayVarP(&moduleNames, "module", "m", nil, "module workflow(s) to execute (can specify multiple)")
scanCmd.Flags().StringArrayVarP(&targets, "target", "t", nil, "target(s) to run against (can be specified multiple times)")
scanCmd.Flags().StringVarP(&targetFile, "target-file", "T", "", "file containing targets (one per line)")
scanCmd.Flags().StringArrayVarP(&paramFlags, "params", "p", nil, "additional parameters (key=value format)")
scanCmd.Flags().StringVarP(&paramsFile, "params-file", "P", "", "file containing parameters (JSON or YAML key:value pairs)")
scanCmd.Flags().StringVarP(&workspacePath, "workspace", "w", "", "custom workspace path")
scanCmd.Flags().BoolVar(&dryRun, "dry-run", false, "show what would be executed without running commands")
scanCmd.Flags().IntVar(&threadsHold, "threads-hold", 0, "override thread count (0 = use tactic default)")
scanCmd.Flags().IntVarP(&concurrency, "concurrency", "c", 1, "number of targets to run concurrently")
scanCmd.Flags().StringVarP(&runTactic, "tactic", "B", "default", "run tactic: aggressive, default, gently")
scanCmd.Flags().StringArrayVarP(&excludeModules, "exclude", "x", nil, "module(s) to exclude from execution (can be specified multiple times)")
scanCmd.Flags().StringVarP(&spaceName, "space", "S", "", "override {{TargetSpace}} variable")
scanCmd.Flags().StringVarP(&workspacesFolder, "workspaces-folder", "W", "", "override {{Workspaces}} variable")
scanCmd.Flags().StringVar(&heuristicsCheck, "heuristics-check", "basic", "heuristics check level: none, basic, advanced")
scanCmd.Flags().BoolVarP(&distributedRun, "distributed-run", "D", false, "submit run to distributed worker queue (requires Redis)")
scanCmd.Flags().StringVar(&redisURLRun, "redis-url", "", "Redis connection URL for distributed mode (overrides settings)")
scanCmd.Flags().BoolVar(&repeatRun, "repeat", false, "repeat run after completion")
scanCmd.Flags().StringVar(&repeatWaitTime, "repeat-wait-time", "1h", "wait time between repeats (e.g., 30s, 20m, 10h, 1d)")
scanCmd.Flags().StringVar(&runTimeout, "timeout", "", "run timeout (e.g., 2h, 3h, 1d)")
scanCmd.Flags().BoolVar(&stdModule, "std-module", false, "read module YAML from stdin")
scanCmd.Flags().BoolVar(&emptyTarget, "empty-target", false, "run without target (generates placeholder target)")
scanCmd.Flags().BoolVarP(&progressBar, "progress-bar", "G", false, "show progress bar during execution (enables silent mode)")
}
+166
View File
@@ -0,0 +1,166 @@
package cli
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/distributed"
"github.com/j3ssie/osmedeus/v5/internal/logger"
"github.com/j3ssie/osmedeus/v5/pkg/server"
"github.com/spf13/cobra"
"go.uber.org/zap"
)
var (
serverHost string
serverPort int
noAuth bool
masterMode bool
redisURLServe string
)
// serveCmd represents the serve command
var serveCmd = &cobra.Command{
Use: "serve",
Aliases: []string{"server"},
Short: "Start the Osmedeus web server",
Long: UsageServe(),
RunE: runServer,
}
func init() {
serveCmd.Flags().StringVar(&serverHost, "host", "", "host to bind (default from config)")
serveCmd.Flags().IntVar(&serverPort, "port", 0, "port to listen on (default from config)")
serveCmd.Flags().BoolVarP(&noAuth, "no-auth", "A", false, "disable all authentication")
serveCmd.Flags().BoolVar(&masterMode, "master", false, "run as distributed master node (requires Redis)")
serveCmd.Flags().StringVar(&redisURLServe, "redis-url", "", "Redis connection URL for master mode (overrides settings)")
}
func runServer(cmd *cobra.Command, args []string) error {
log := logger.Get()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
// Override with flags if provided
if serverHost != "" {
cfg.Server.Host = serverHost
}
if serverPort != 0 {
cfg.Server.Port = serverPort
}
// Show critical warning if running without authentication
if noAuth {
fmt.Println("\033[31m\033[1m")
fmt.Println("╔════════════════════════════════════════════════════════════════╗")
fmt.Println("║ ⚠️ CRITICAL WARNING: Server running WITHOUT authentication ║")
fmt.Println("║ Anyone can access all API endpoints without credentials! ║")
fmt.Println("║ Only use -A flag for development/testing purposes. ║")
fmt.Println("╚════════════════════════════════════════════════════════════════╝")
fmt.Println("\033[0m")
}
// Handle shutdown signals
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
log.Info("Shutting down...")
cancel()
}()
// Start master node if --master flag is set
var master *distributed.Master
if masterMode {
// Override Redis config if --redis-url provided
if redisURLServe != "" {
redisCfg, err := distributed.ParseRedisURL(redisURLServe)
if err != nil {
return fmt.Errorf("invalid redis URL: %w", err)
}
cfg.Redis = *redisCfg
}
if !cfg.IsRedisConfigured() {
return fmt.Errorf("redis not configured. Add redis section to osm-settings.yaml or use --redis-url")
}
var err error
master, err = distributed.NewMaster(cfg)
if err != nil {
return fmt.Errorf("failed to create master: %w", err)
}
// Start master in background
go func() {
if err := master.Start(ctx); err != nil {
log.Error("Master error", zap.Error(err))
}
}()
log.Info("Started distributed master node")
}
// Create server with master reference for distributed endpoints
opts := &server.Options{
NoAuth: noAuth,
Master: master,
Debug: debug,
}
srv, err := server.New(cfg, opts)
if err != nil {
log.Error("Failed to create server", zap.Error(err))
return err
}
// Log debug mode status
if debug {
log.Info("Debug mode enabled - request bodies and detailed errors will be logged")
}
// Start server in goroutine
serverErr := make(chan error, 1)
go func() {
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
log.Info("Starting Osmedeus server", zap.String("address", addr))
// Log swagger access URL
swaggerURL := fmt.Sprintf("http://%s:%d/swagger/", cfg.Server.Host, cfg.Server.Port)
log.Info("Access Swagger documentation", zap.String("url", swaggerURL))
serverErr <- srv.Start(addr)
}()
// Wait for shutdown or server error
select {
case <-ctx.Done():
log.Info("Shutting down server...")
// Give server 5 seconds to shutdown gracefully
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := srv.ShutdownWithContext(shutdownCtx); err != nil {
log.Error("Error during shutdown", zap.Error(err))
}
log.Info("Server stopped")
case err := <-serverErr:
if err != nil {
return fmt.Errorf("server error: %w", err)
}
}
return nil
}
+271
View File
@@ -0,0 +1,271 @@
package cli
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/snapshot"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/spf13/cobra"
)
var (
snapshotOutputPath string
snapshotForce bool
snapshotSkipDB bool
)
var snapshotCmd = &cobra.Command{
Use: "snapshot",
Short: "Export and import workspace snapshots",
Long: UsageSnapshot(),
}
var snapshotExportCmd = &cobra.Command{
Use: "export <workspace>",
Short: "Export workspace to compressed zip archive",
Long: `Export a workspace folder to a compressed zip archive.
The archive is created with highest compression (level 9) and stored in
the snapshot folder (default: ~/osmedeus-base/snapshot/).
Examples:
osmedeus snapshot export example.com
osmedeus snapshot export example.com -o /tmp/backup.zip`,
Args: cobra.ExactArgs(1),
RunE: runSnapshotExport,
}
var snapshotImportCmd = &cobra.Command{
Use: "import <source>",
Short: "Import workspace from zip file or URL",
Long: `Import a workspace from a zip file or URL.
The source can be a local file path or a URL to download.
The workspace will be extracted to the workspaces folder and
optionally imported to the database with data_source="imported".
Examples:
osmedeus snapshot import ~/snapshot/example.com_1234567890.zip
osmedeus snapshot import https://example.com/workspace.zip
osmedeus snapshot import ~/backup.zip --force`,
Args: cobra.ExactArgs(1),
RunE: runSnapshotImport,
}
var snapshotListCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List available snapshots",
Long: `List all snapshot files in the snapshot folder.`,
RunE: runSnapshotList,
}
func init() {
// Export flags
snapshotExportCmd.Flags().StringVarP(&snapshotOutputPath, "output", "o", "", "Custom output path for the snapshot")
// Import flags
snapshotImportCmd.Flags().BoolVarP(&snapshotForce, "force", "f", false, "Overwrite existing workspace")
snapshotImportCmd.Flags().BoolVar(&snapshotSkipDB, "skip-db", false, "Skip database import (files only)")
// Add subcommands
snapshotCmd.AddCommand(snapshotExportCmd)
snapshotCmd.AddCommand(snapshotImportCmd)
snapshotCmd.AddCommand(snapshotListCmd)
}
func runSnapshotExport(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
workspaceName := args[0]
workspacePath := filepath.Join(cfg.WorkspacesPath, workspaceName)
// Check if workspace exists
if _, err := os.Stat(workspacePath); os.IsNotExist(err) {
printer.Error("Workspace not found: %s", workspacePath)
return fmt.Errorf("workspace not found: %s", workspaceName)
}
// Determine output path
outputPath := snapshotOutputPath
if outputPath == "" {
// Ensure snapshot directory exists
if err := os.MkdirAll(cfg.SnapshotPath, 0755); err != nil {
return fmt.Errorf("failed to create snapshot directory: %w", err)
}
outputPath = filepath.Join(cfg.SnapshotPath, fmt.Sprintf("%s_%d.zip", workspaceName, getTimestamp()))
}
printer.Info("Exporting workspace: %s", workspaceName)
printer.Info("Source: %s", workspacePath)
printer.Info("Destination: %s", outputPath)
result, err := snapshot.ExportWorkspace(workspacePath, outputPath)
if err != nil {
printer.Error("Export failed: %s", err)
return err
}
printer.Success("Snapshot created successfully!")
printer.Info("File: %s", result.OutputPath)
printer.Info("Size: %s", formatBytes(result.FileSize))
return nil
}
func runSnapshotImport(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
source := args[0]
// Show warning
printer.Warning("WARNING: Workspace Snapshot Import")
fmt.Println()
fmt.Println("Only import snapshots from trusted sources!")
fmt.Println()
fmt.Println("Imported workspace data may contain:")
fmt.Println("- Database records that could conflict with existing data")
fmt.Println("- File paths that reference external resources")
fmt.Println("- Configuration that may not be compatible")
fmt.Println()
fmt.Println("The imported workspace database state may be unstable.")
fmt.Println()
// Ask for confirmation
if !confirmPrompt("Continue with import?") {
printer.Info("Import cancelled.")
return nil
}
printer.Info("Importing from: %s", source)
var result *snapshot.ImportResult
var err error
if snapshotForce {
result, err = snapshot.ForceImportWorkspace(source, cfg.WorkspacesPath, snapshotSkipDB, cfg)
} else {
result, err = snapshot.ImportWorkspace(source, cfg.WorkspacesPath, snapshotSkipDB, cfg)
}
if err != nil {
printer.Error("Import failed: %s", err)
return err
}
printer.Success("Workspace imported successfully!")
printer.Info("Workspace: %s", result.WorkspaceName)
printer.Info("Location: %s", result.LocalPath)
printer.Info("Data Source: %s", result.DataSource)
printer.Info("Files: %d", result.FilesCount)
if snapshotSkipDB {
printer.Info("Database import skipped (--skip-db)")
}
return nil
}
func runSnapshotList(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
cfg := config.Get()
if cfg == nil {
return fmt.Errorf("configuration not loaded")
}
snapshots, err := snapshot.ListSnapshots(cfg.SnapshotPath)
if err != nil {
printer.Error("Failed to list snapshots: %s", err)
return err
}
if len(snapshots) == 0 {
printer.Info("No snapshots found in: %s", cfg.SnapshotPath)
return nil
}
printer.Section("Available Snapshots")
fmt.Println()
for _, s := range snapshots {
fmt.Printf(" %-50s %10s %s\n",
s.Name,
formatBytes(s.Size),
s.CreatedAt.Format("2006-01-02 15:04:05"),
)
}
fmt.Println()
printer.Info("Total: %d snapshots in %s", len(snapshots), cfg.SnapshotPath)
return nil
}
// confirmPrompt asks for user confirmation
func confirmPrompt(message string) bool {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("%s [y/N]: ", message)
response, err := reader.ReadString('\n')
if err != nil {
return false
}
response = strings.TrimSpace(strings.ToLower(response))
return response == "y" || response == "yes"
}
// formatBytes formats bytes to human readable string
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// getTimestamp returns current Unix timestamp
func getTimestamp() int64 {
return time.Now().Unix()
}
// UsageSnapshot returns usage text for snapshot command
func UsageSnapshot() string {
return `Workspace snapshot management for backup and sharing.
Export creates a compressed zip archive of a workspace folder.
Import extracts a snapshot and optionally imports to database.
Commands:
export <workspace> Export workspace to zip archive
import <source> Import workspace from zip file or URL
list List available snapshots
Examples:
osmedeus snapshot export example.com
osmedeus snapshot import ~/backup.zip
osmedeus snapshot list
`
}
+193
View File
@@ -0,0 +1,193 @@
package cli
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"time"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/j3ssie/osmedeus/v5/internal/updater"
"github.com/spf13/cobra"
)
var (
updateCheck bool
updateYes bool
updateForce bool
updateVersion string
)
// updateCmd represents the update command
var updateCmd = &cobra.Command{
Use: "update",
Short: "Update osmedeus to the latest version",
Long: UsageUpdate(),
RunE: runUpdate,
}
func init() {
updateCmd.Flags().BoolVar(&updateCheck, "check", false, "only check for updates without installing")
updateCmd.Flags().BoolVarP(&updateYes, "yes", "y", false, "skip confirmation prompt")
updateCmd.Flags().BoolVar(&updateForce, "force", false, "force update even if current version is latest")
updateCmd.Flags().StringVar(&updateVersion, "version", "", "update to a specific version (e.g., v5.1.0)")
}
func runUpdate(cmd *cobra.Command, args []string) error {
printer := terminal.NewPrinter()
// Parse owner/repo from REPO_URL
owner, repo, err := updater.ParseRepoURL(core.REPO_URL)
if err != nil {
return fmt.Errorf("failed to parse repository URL: %w", err)
}
// Create updater
upd := updater.DefaultUpdater(owner, repo)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
currentVersion := core.VERSION
printer.Info("Current version: %s", terminal.Cyan(currentVersion))
// Check for updates
var release *updater.Release
var hasUpdate bool
if updateVersion != "" {
// Check for specific version
printer.Info("Checking for version %s...", terminal.Cyan(updateVersion))
release, _, err = upd.CheckSpecificVersion(ctx, updateVersion)
if err != nil {
return fmt.Errorf("failed to check for version: %w", err)
}
if release == nil {
return fmt.Errorf("version %s not found", updateVersion)
}
// For specific version, hasUpdate means version exists
hasUpdate = true
} else {
// Check for latest
printer.Info("Checking for updates...")
release, hasUpdate, err = upd.CheckForUpdate(ctx, currentVersion)
if err != nil {
return fmt.Errorf("failed to check for updates: %w", err)
}
}
if release == nil {
printer.Info("No releases found")
return nil
}
if !hasUpdate && !updateForce {
printer.Success("You are running the latest version (%s)", currentVersion)
return nil
}
// Display release info
printer.Newline()
printer.Section("New Version Available")
printer.KeyValue("Version", terminal.Green(release.Version))
if !release.PublishedAt.IsZero() {
printer.KeyValue("Published", release.PublishedAt.Format("2006-01-02"))
}
if release.ReleaseNotes != "" {
printer.Newline()
printer.SubSection("Release Notes")
// Truncate long notes
notes := release.ReleaseNotes
if len(notes) > 500 {
notes = notes[:500] + "..."
}
fmt.Println(notes)
}
printer.Newline()
// Check-only mode
if updateCheck {
printer.Info("Run 'osmedeus update' to install this version")
return nil
}
// Confirm update
if !updateYes {
printer.Print("Do you want to update to %s? [y/N]: ", terminal.Green(release.Version))
if !confirmUpdatePrompt() {
printer.Info("Update cancelled")
return nil
}
}
// Perform update
printer.Info("Downloading %s...", release.Version)
var result *updater.UpdateResult
if updateVersion != "" {
result, err = upd.UpdateToVersion(ctx, currentVersion, updateVersion, updateForce)
} else {
result, err = upd.Update(ctx, currentVersion, updateForce)
}
if err != nil {
return fmt.Errorf("update failed: %w", err)
}
if result.Updated {
printer.Success("Successfully updated from %s to %s",
terminal.Gray(result.OldVersion),
terminal.Green(result.NewVersion))
printer.Info("Restart osmedeus to use the new version")
} else {
printer.Info("No update was performed")
}
return nil
}
// confirmUpdatePrompt reads y/n confirmation from stdin
func confirmUpdatePrompt() bool {
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return false
}
input = strings.TrimSpace(strings.ToLower(input))
return input == "y" || input == "yes"
}
// UsageUpdate returns the Long description for the update command
func UsageUpdate() string {
return terminal.BoldCyan("◆ Description") + `
Update osmedeus binary to the latest version from GitHub releases.
Compares semantic versions and downloads the appropriate binary for
your platform.
` + terminal.BoldCyan("▶ Options") + `
` + terminal.Yellow("--check") + ` - Only check for updates without installing
` + terminal.Yellow("--yes") + ` - Skip confirmation prompt
` + terminal.Yellow("--force") + ` - Force update even if already on latest version
` + terminal.Yellow("--version") + ` - Update to a specific version
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# Check for updates") + `
osmedeus update ` + terminal.Yellow("--check") + `
` + terminal.Green("# Update to latest version") + `
osmedeus update
` + terminal.Green("# Update without confirmation") + `
osmedeus update ` + terminal.Yellow("--yes") + `
` + terminal.Green("# Force reinstall current version") + `
osmedeus update ` + terminal.Yellow("--force") + `
` + terminal.Green("# Update to specific version") + `
osmedeus update ` + terminal.Yellow("--version") + ` v5.1.0
` + docsFooter()
}
+893
View File
@@ -0,0 +1,893 @@
package cli
import (
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
)
// UsageRoot returns the Long description for the root command
func UsageRoot() string {
return terminal.BoldCyan("◆ Description") + `
Osmedeus is a powerful workflow engine for executing automated
reconnaissance and security assessment workflows.
It supports both module (single execution units) and flow (multi-module
orchestration) workflows with parallel and sequential execution patterns.
` + terminal.BoldCyan("▶ Key Features") + `
• Execute YAML-defined security workflows
• Support for parallel and sequential execution
• Distributed scanning with master/worker architecture
• Template variables and utility functions
` + terminal.BoldCyan("▷ Quick Start") + `
` + terminal.Green("# Run a module workflow") + `
osmedeus run ` + terminal.Yellow("-m") + ` simple-module ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Run a flow workflow") + `
osmedeus run ` + terminal.Yellow("-f") + ` recon-workflow ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Evaluate a utility function") + `
osmedeus func e 'log_info("Hello {{target}}")' ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# List available workflows") + `
osmedeus workflow list
` + terminal.Green("# Show all usage examples") + `
osmedeus ` + terminal.Yellow("--usage-example") + `
` + docsFooter()
}
// UsageRun returns the Long description for the run command
func UsageRun() string {
return terminal.BoldCyan("◆ Description") + `
Execute a workflow against one or more targets.
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# Run against a single target") + `
osmedeus run ` + terminal.Yellow("-f") + ` recon-workflow ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Run against multiple targets") + `
osmedeus run ` + terminal.Yellow("-m") + ` simple-module ` + terminal.Yellow("-t") + ` target1.com ` + terminal.Yellow("-t") + ` target2.com
` + terminal.Green("# Run with stdin input with concurrency") + `
cat list-of-urls.txt | osmedeus run ` + terminal.Yellow("-m") + ` simple-module ` + terminal.Yellow("--concurrency") + ` 10
` + terminal.Green("# Combine multiple input methods") + `
echo "extra.com" | osmedeus run ` + terminal.Yellow("-m") + ` simple-module ` + terminal.Yellow("-t") + ` main.com ` + terminal.Yellow("-T") + ` more-targets.txt
` + terminal.Green("# Run with custom parameters") + `
osmedeus run ` + terminal.Yellow("-m") + ` simple-module ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--params") + ` 'threads=20'
` + terminal.Green("# Run with custom base folder") + `
osmedeus run ` + terminal.Yellow("--base-folder") + ` /opt/osmedeus-base ` + terminal.Yellow("-f") + ` recon-workflow ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Run with timeout (cancel if exceeds)") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--timeout") + ` 2h
` + terminal.Green("# Repeat run every hour continuously") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--repeat") + ` ` + terminal.Yellow("--repeat-wait-time") + ` 1h
` + terminal.Green("# Run multiple modules in sequence") + `
osmedeus run ` + terminal.Yellow("-m") + ` subdomain ` + terminal.Yellow("-m") + ` portscan ` + terminal.Yellow("-m") + ` vulnscan ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Combine timeout with repeat mode") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--timeout") + ` 3h ` + terminal.Yellow("--repeat") + ` ` + terminal.Yellow("--repeat-wait-time") + ` 30m
` + terminal.Green("# Dry-run mode (preview without executing)") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--dry-run") + `
` + terminal.Green("# Run module from stdin (pipe YAML)") + `
cat module.yaml | osmedeus run ` + terminal.Yellow("--std-module") + ` ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Load parameters from YAML/JSON file") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--params-file") + ` params.yaml
` + terminal.Green("# Custom workspace path") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--workspace") + ` /custom/workspace
` + terminal.Green("# Skip heuristics checks") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--heuristics-check") + ` none
` + terminal.Green("# Concurrent targets from file") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-T") + ` targets.txt ` + terminal.Yellow("--concurrency") + ` 5
` + docsFooter()
}
// UsageServe returns the Long description for the serve command
func UsageServe() string {
return terminal.BoldCyan("◆ Description") + `
Start the Osmedeus web server that provides REST API endpoints.
` + terminal.BoldCyan("▶ Features") + `
• REST API for managing runs
• Workflow listing and management
• Real-time run progress via WebSocket
• Settings management
Use ` + terminal.Yellow("--master") + ` to run as a distributed master node that coordinates
workers connected via Redis.
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# Start server with default settings") + `
osmedeus serve
` + terminal.Green("# Start server on custom port") + `
osmedeus serve ` + terminal.Yellow("--port") + ` 8080
` + terminal.Green("# Start server without authentication (development only)") + `
osmedeus serve ` + terminal.Yellow("-A") + `
` + terminal.Green("# Start server on specific host without auth") + `
osmedeus serve ` + terminal.Yellow("--host") + ` 127.0.0.1 ` + terminal.Yellow("--port") + ` 8811 ` + terminal.Yellow("-A") + `
` + terminal.Green("# Start as distributed master node") + `
osmedeus serve ` + terminal.Yellow("--master") + `
` + docsFooter()
}
// UsageWorkflow returns the Long description for the workflow command
func UsageWorkflow() string {
return terminal.BoldCyan("◆ Description") + `
Commands for listing, viewing, and validating workflows.
` + terminal.BoldCyan("▶ Subcommands") + `
` + terminal.Yellow("list") + ` - List available workflows
` + terminal.Yellow("show") + ` - Show workflow details
` + terminal.Yellow("validate") + ` - Validate a workflow
` + terminal.BoldCyan("▶ Workflow Preferences") + `
Workflows can define execution preferences in YAML that act as defaults.
CLI flags always take precedence over workflow preferences.
` + terminal.Yellow("preferences:") + `
` + terminal.Gray("disable_notifications: false") + ` # --disable-notification
` + terminal.Gray("disable_logging: true") + ` # --disable-logging
` + terminal.Gray("heuristics_check: 'basic'") + ` # --heuristics-check
` + terminal.Gray("ci_output_format: true") + ` # --ci-output-format
` + terminal.Gray("silent: true") + ` # --silent
` + terminal.Gray("repeat: true") + ` # --repeat
` + terminal.Gray("repeat_wait_time: '60s'") + ` # --repeat-wait-time
` + docsFooter()
}
// UsageFunction returns the Long description for the function command
func UsageFunction() string {
return terminal.BoldCyan("◆ Description") + `
Execute and test utility functions available in workflows.
` + terminal.BoldCyan("▶ Subcommands") + `
` + terminal.Yellow("list") + ` - List all available functions
` + terminal.Yellow("eval (e)") + ` - Evaluate scripts with template rendering
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# List all available functions") + `
osmedeus func list
` + terminal.Green("# Evaluate a simple function") + `
osmedeus func eval 'trim(" hello ")'
` + terminal.Green("# Short alias for eval") + `
osmedeus func e 'log_info("Hello World")'
` + terminal.Green("# Use with target variable") + `
osmedeus func e 'fileExists("{{target}}")' ` + terminal.Yellow("-t") + ` /tmp/test.txt
` + terminal.Green("# Print markdown file with syntax highlighting") + `
osmedeus func e 'print_markdown_from_file("README.md")'
` + terminal.Green("# Multi-line script with variable") + `
osmedeus func e 'var x = trim(" test "); log_info(x); x'
` + terminal.Green("# Make HTTP request") + `
osmedeus func e 'httpRequest("https://api.example.com", "GET", {}, "")'
` + terminal.Green("# With custom params") + `
osmedeus func e 'log_info("{{host}}:{{port}}")' ` + terminal.Yellow("--params") + ` 'host=localhost' ` + terminal.Yellow("--params") + ` 'port=8080'
` + terminal.Green("# Use -f flag for shell path autocompletion on file arguments") + `
osmedeus func e ` + terminal.Yellow("-f") + ` trim " hello world "
osmedeus func e ` + terminal.Yellow("-f") + ` fileExists /tmp
osmedeus func e ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("-f") + ` log_info "Processing {{target}}"
` + terminal.Green("# Query database with SQL") + `
osmedeus func e 'db_select("SELECT severity, COUNT(*) FROM vulnerabilities GROUP BY severity", "markdown")'
` + terminal.Green("# Query filtered assets from database") + `
osmedeus func e 'db_select_assets_filtered("example.com", 200, "subdomain", "jsonl")'
` + terminal.Green("# Read script from stdin") + `
echo 'log_info("hello")' | osmedeus func e ` + terminal.Yellow("--stdin") + `
` + terminal.Green("# Alternative stdin syntax") + `
echo 'trim(" test ")' | osmedeus func e -
` + docsFooter()
}
// UsageFunctionEval returns the Long description for the function eval command
func UsageFunctionEval() string {
return terminal.BoldCyan("◆ Description") + `
Evaluate a script with template rendering and function execution.
` + terminal.BoldCyan("▶ Processing Phases") + `
1. Template variables ({{target}}, {{custom}}) are rendered
2. The result is executed as JavaScript with access to utility functions
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# Print markdown file with syntax highlighting") + `
osmedeus func e 'print_markdown_from_file("README.md")'
` + terminal.Green("# Log a message with INFO prefix") + `
osmedeus func e 'log_info("Scan completed for {{target}}")' ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Save content to file") + `
osmedeus func e 'save_content(render_markdown_from_file("README.md"), "/tmp/output.txt")'
` + terminal.Green("# Use with variable") + `
osmedeus func e 'var content = "sample"; save_content(content, "/tmp/output.txt")'
` + terminal.Green("# Sort a file using Unix sort") + `
osmedeus func e 'sortUnix("/tmp/input.txt", "/tmp/sorted.txt")'
` + terminal.Green("# Make HTTP request") + `
osmedeus func e 'httpRequest("https://api.example.com", "GET", {}, "")'
` + terminal.Green("# With custom params") + `
osmedeus func e 'log_info("{{host}}:{{port}}")' ` + terminal.Yellow("--params") + ` 'host=localhost' ` + terminal.Yellow("--params") + ` 'port=8080'
` + terminal.Green("# Use -f flag for shell path autocompletion on file arguments") + `
osmedeus func e ` + terminal.Yellow("-f") + ` trim " hello world "
osmedeus func e ` + terminal.Yellow("-f") + ` fileExists /tmp
osmedeus func e ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("-f") + ` log_info "Processing {{target}}"
` + terminal.Green("# Query database - get vulnerability counts by severity") + `
osmedeus func e 'db_select("SELECT severity, COUNT(*) as count FROM vulnerabilities GROUP BY severity", "markdown")'
` + terminal.Green("# Query database - get filtered assets as JSONL") + `
osmedeus func e 'db_select_assets_filtered("example.com", 200, "subdomain", "jsonl")'
` + terminal.Green("# Query database - get all vulnerabilities for a workspace") + `
osmedeus func e 'db_select_vulnerabilities("example.com", "markdown")'
` + terminal.Green("# Read script from stdin") + `
echo 'print_markdown_from_file("README.md")' | osmedeus func e --stdin
` + terminal.Green("# Alternative stdin syntax") + `
echo 'log_info("hello")' | osmedeus func e -
` + docsFooter()
}
// UsageHealth returns the Long description for the health command
func UsageHealth() string {
return terminal.BoldCyan("◆ Description") + `
Check the Osmedeus environment for issues and fix them.
` + terminal.Gray("This command is an alias for 'osmedeus install validate'.") + `
` + terminal.BoldCyan("✔ Checks Performed") + `
• Base folder, workspaces, workflows folders exist (creates if missing)
• Configuration file is valid (osm-settings.yaml)
• All workflows are valid
` + terminal.BoldCyan("▷ Examples") + `
osmedeus health # using alias
osmedeus install validate # primary command
` + docsFooter()
}
// UsageWorker returns the Long description for the worker command
func UsageWorker() string {
return terminal.BoldCyan("◆ Description") + `
Commands for managing worker nodes in distributed mode.
` + terminal.BoldCyan("▶ Subcommands") + `
` + terminal.Yellow("join") + ` - Join the distributed worker pool
` + terminal.Yellow("status") + ` - Show worker pool status
` + docsFooter()
}
// UsageWorkerJoin returns the Long description for the worker join command
func UsageWorkerJoin() string {
return terminal.BoldCyan("◆ Description") + `
Join the distributed worker pool and start processing tasks.
The worker will connect to Redis and wait for tasks from the master node.
Tasks are executed using the local workflow engine.
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# Join using settings from osm-settings.yaml") + `
osmedeus worker join
` + terminal.Green("# Join using a specific Redis URL") + `
osmedeus worker join ` + terminal.Yellow("--redis-url") + ` redis://user:pass@localhost:6379/0
` + docsFooter()
}
// UsageWorkerStatus returns the Long description for the worker status command
func UsageWorkerStatus() string {
return terminal.BoldCyan("◆ Description") + `
Display the status of all workers connected to the Redis server.
` + docsFooter()
}
// UsageConfig returns the Long description for the config command
func UsageConfig() string {
return terminal.BoldCyan("◆ Description") + `
Manage osmedeus configuration settings.
` + terminal.BoldCyan("▶ Subcommands") + `
` + terminal.Yellow("clean") + ` - Reset configuration to defaults
` + terminal.Yellow("set") + ` - Set a configuration value
` + terminal.Yellow("view") + ` - View a configuration value
` + terminal.Yellow("list") + ` - List configuration values
` + docsFooter()
}
// UsageConfigClean returns the Long description for the config clean command
func UsageConfigClean() string {
return terminal.BoldCyan("◆ Description") + `
Reset the configuration file to default values.
Backs up the existing config to osm-settings.yaml.backup before overwriting.
` + terminal.BoldCyan("▷ Example") + `
` + terminal.Green("osmedeus config clean") + `
` + docsFooter()
}
// UsageConfigSet returns the Long description for the config set command
func UsageConfigSet() string {
return terminal.BoldCyan("◆ Description") + `
Set a configuration value using dot notation.
` + terminal.BoldCyan("▷ Syntax") + `
osmedeus config set <key> <value>
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("osmedeus config set server.port 9000") + `
` + terminal.Green("osmedeus config set server.username admin") + `
` + terminal.Green("osmedeus config set server.password \"d8506b99a052e797f73d1dab\"") + `
` + terminal.Green("osmedeus config set server.jwt.secret_signing_key \"d8506b99a052e797f73d1dab\"") + `
` + terminal.Green("osmedeus config set scan_tactic.default 20") + `
` + terminal.Green("osmedeus config set global_vars.github_token ghp_xxx") + `
` + terminal.Green("osmedeus config set notification.enabled true") + `
` + terminal.BoldCyan("▷ Available Keys") + `
` + terminal.Yellow("base_folder") + ` Base directory path
` + terminal.Yellow("server.host") + ` Server bind host
` + terminal.Yellow("server.port") + ` Server port number
` + terminal.Yellow("server.username") + ` Auth username
` + terminal.Yellow("server.password") + ` Auth password
` + terminal.Yellow("server.simple_user_map_key.<username>") + ` Auth user password by username
` + terminal.Yellow("server.jwt.secret_signing_key") + ` JWT secret signing key
` + terminal.Yellow("server.jwt.expiration_minutes") + ` JWT expiration time in minutes
` + terminal.Yellow("server.ui_path") + ` UI static files path
` + terminal.Yellow("server.enabled_auth_api") + ` Enable API key auth (true/false)
` + terminal.Yellow("server.auth_api_key") + ` API key for x-osm-api-key header
` + terminal.Yellow("database.db_engine") + ` sqlite or postgresql
` + terminal.Yellow("database.host") + ` Database host
` + terminal.Yellow("database.port") + ` Database port
` + terminal.Yellow("scan_tactic.aggressive") + ` Aggressive mode threads
` + terminal.Yellow("scan_tactic.default") + ` Default mode threads
` + terminal.Yellow("scan_tactic.gently") + ` Gentle mode threads
` + terminal.Yellow("redis.host") + ` Redis host
` + terminal.Yellow("redis.port") + ` Redis port
` + terminal.Yellow("global_vars.<name>") + ` Set a global variable
` + terminal.Yellow("notification.enabled") + ` Enable notifications (true/false)
` + terminal.Yellow("notification.telegram.bot_token") + ` Telegram bot token
` + terminal.Yellow("environments.external_binaries_path") + ` Binaries directory
` + terminal.Yellow("storage.enabled") + ` Enable cloud storage (true/false)
` + docsFooter()
}
func UsageConfigView() string {
return terminal.BoldCyan("◆ Description") + `
View a configuration value using dot notation.
` + terminal.BoldCyan("▷ Syntax") + `
osmedeus config view <key>
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("osmedeus config view server.port") + `
` + terminal.Green("osmedeus config view server.username") + `
` + terminal.Green("osmedeus config view server.password") + `
` + terminal.Green("osmedeus config view server.jwt.secret_signing_key") + `
` + terminal.Green("osmedeus config view server.jwt.secret_signing_key --redact") + `
` + docsFooter()
}
func UsageConfigList() string {
return terminal.BoldCyan("◆ Description") + `
List configuration values in dot notation.
` + terminal.BoldCyan("▷ Syntax") + `
osmedeus config list
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("osmedeus config list") + `
` + terminal.Green("osmedeus config list --show-secrets") + `
` + docsFooter()
}
// UsageDB returns the Long description for the db command
func UsageDB() string {
return terminal.BoldCyan("◆ Description") + `
Database management commands for seeding and cleaning data.
` + terminal.BoldCyan("▶ Subcommands") + `
` + terminal.Yellow("list") + ` - List database tables and row counts
` + terminal.Yellow("seed") + ` - Seed database with sample data
` + terminal.Yellow("clean") + ` - Remove all data from database
` + terminal.Yellow("migrate") + ` - Run database migrations
` + docsFooter()
}
// UsageDBSeed returns the Long description for the db seed command
func UsageDBSeed() string {
return terminal.BoldCyan("◆ Description") + `
Seed the database with sample data for development and testing.
This command populates the database with realistic sample records including:
• Runs (completed, running, failed examples)
• Step results (subfinder, httpx, nuclei, etc.)
• Artifacts (subdomains.txt, alive-hosts.txt, etc.)
• Assets (HTTP endpoints with status codes and tech stacks)
• Event logs (run events, asset discoveries)
• Schedules (daily recon, weekly vuln scan)
` + terminal.BoldCyan("▷ Example") + `
` + terminal.Green("osmedeus db seed") + `
` + docsFooter()
}
// UsageDBClean returns the Long description for the db clean command
func UsageDBClean() string {
return terminal.BoldCyan("◆ Description") + `
Remove all data from all database tables.
` + terminal.Yellow("WARNING:") + ` This is a destructive operation that cannot be undone.
Use the --force flag to skip the confirmation prompt.
` + terminal.BoldCyan("▷ Example") + `
` + terminal.Green("osmedeus db clean --force") + `
` + docsFooter()
}
// UsageDBMigrate returns the Long description for the db migrate command
func UsageDBMigrate() string {
return terminal.BoldCyan("◆ Description") + `
Run database migrations to create or update tables.
This command ensures all required tables exist with the correct schema.
Safe to run multiple times (uses IF NOT EXISTS).
` + terminal.BoldCyan("▷ Example") + `
` + terminal.Green("osmedeus db migrate") + `
` + docsFooter()
}
// UsageDBList returns the Long description for the db list command
func UsageDBList() string {
return terminal.BoldCyan("◆ Description") + `
List all database tables with their row counts, or list records from a
specific table with pagination support.
` + terminal.BoldCyan("▶ Options") + `
` + terminal.Yellow("-t, --table") + ` Table name to list records from
` + terminal.Yellow("--offset") + ` Number of records to skip (default: 0)
` + terminal.Yellow("--limit") + ` Maximum records to return (default: 20, max: 100)
` + terminal.Yellow("--list-columns") + ` List all available columns for the specified table
` + terminal.Yellow("--exclude-columns") + ` Comma-separated column names to exclude from output
` + terminal.BoldCyan("▶ Valid Tables") + `
runs, step_results, artifacts, assets, event_logs, schedules
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# List all tables with row counts") + `
osmedeus db list
` + terminal.Green("# List records from runs table") + `
osmedeus db list ` + terminal.Yellow("-t") + ` runs
` + terminal.Green("# List available columns for assets table") + `
osmedeus db list ` + terminal.Yellow("-t") + ` assets ` + terminal.Yellow("--list-columns") + `
` + terminal.Green("# List assets excluding specific columns") + `
osmedeus db list ` + terminal.Yellow("-t") + ` assets ` + terminal.Yellow("--exclude-columns") + ` id,created_at,updated_at
` + terminal.Green("# List assets with pagination") + `
osmedeus db list ` + terminal.Yellow("-t") + ` assets ` + terminal.Yellow("--offset") + ` 0 ` + terminal.Yellow("--limit") + ` 10
` + terminal.Green("# Get next page of results") + `
osmedeus db list ` + terminal.Yellow("-t") + ` assets ` + terminal.Yellow("--offset") + ` 10 ` + terminal.Yellow("--limit") + ` 10
` + docsFooter()
}
// UsageInstall returns the Long description for the install command
func UsageInstall() string {
return terminal.BoldCyan("◆ Description") + `
Install workflows, base folder, or binaries from various sources.
` + terminal.BoldCyan("▶ Subcommands") + `
` + terminal.Yellow("workflow") + ` - Install workflows from git URL, zip URL, or local zip
` + terminal.Yellow("base") + ` - Install base folder (backs up and restores database)
` + terminal.Yellow("binary") + ` - Install binaries from registry
` + terminal.Yellow("env") + ` - Add binaries path to shell configuration
` + terminal.Yellow("validate") + ` - Check and fix environment health
` + terminal.BoldCyan("▷ Examples") + `
` + terminal.Green("# List available binaries (direct-fetch mode)") + `
osmedeus install binary ` + terminal.Yellow("--list-registry-direct-fetch") + `
` + terminal.Green("# List available binaries (nix-build mode)") + `
osmedeus install binary ` + terminal.Yellow("--list-registry-nix-build") + `
` + terminal.Green("# Install specific binaries") + `
osmedeus install binary ` + terminal.Yellow("--name") + ` nuclei ` + terminal.Yellow("--name") + ` httpx
` + terminal.Green("# Install all required binaries") + `
osmedeus install binary ` + terminal.Yellow("--all") + `
` + terminal.Green("# Install all binaries including optional ones") + `
osmedeus install binary ` + terminal.Yellow("--all") + ` ` + terminal.Yellow("--install-optional") + `
` + terminal.Green("# Check if binaries are installed") + `
osmedeus install binary ` + terminal.Yellow("--all") + ` ` + terminal.Yellow("--check") + `
` + terminal.Green("# Install Nix package manager") + `
osmedeus install binary ` + terminal.Yellow("--nix-installation") + `
` + terminal.Green("# Install binary via Nix") + `
osmedeus install binary ` + terminal.Yellow("--name") + ` nuclei ` + terminal.Yellow("--nix-build-install") + `
` + terminal.Green("# Install all binaries via Nix") + `
osmedeus install binary ` + terminal.Yellow("--all") + ` ` + terminal.Yellow("--nix-build-install") + `
` + terminal.Green("# Install workflows from git or from a zip URL or from a local zip file") + `
osmedeus install workflow https://github.com/user/osmedeus-workflows.git
osmedeus install workflow http://<custom-host>/workflow-osmedeus.zip
osmedeus install workflow local-file-workflow-osmedeus.zip
` + terminal.Green("# Install base folder from git") + `
osmedeus install base https://github.com/user/osmedeus-base.git
osmedeus install base http://<custom-host>/osmedeus-base.zip
osmedeus install base local-file-osmedeus-base.zip
` + docsFooter()
}
// UsageAllExamples returns comprehensive usage examples for all commands
func UsageAllExamples() string {
return terminal.BoldCyan("▶ Run Examples") + `
` + terminal.Green("# Basic module run") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Flow workflow run") + `
osmedeus run ` + terminal.Yellow("-f") + ` general ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Multiple targets") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` target1.com ` + terminal.Yellow("-t") + ` target2.com
` + terminal.Green("# Stdin input") + `
cat urls.txt | osmedeus run ` + terminal.Yellow("-m") + ` recon
` + terminal.Green("# Run with stdin input with concurrency") + `
cat list-of-urls.txt | osmedeus run ` + terminal.Yellow("-m") + ` simple-module ` + terminal.Yellow("--concurrency") + ` 10
` + terminal.Green("# With custom parameters") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--params") + ` 'threads=50'
` + terminal.Green("# Parameters from YAML file") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--params-file") + ` params.yaml
` + terminal.Green("# Dry-run mode") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--dry-run") + `
` + terminal.Green("# Run module from stdin YAML") + `
cat module.yaml | osmedeus run ` + terminal.Yellow("--std-module") + ` ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Custom workspace") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--workspace") + ` /path/to/workspace
` + terminal.Green("# With timeout") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--timeout") + ` 2h
` + terminal.Green("# Repeat run continuously") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--repeat") + ` ` + terminal.Yellow("--repeat-wait-time") + ` 1h
` + terminal.Green("# Run multiple modules in sequence") + `
osmedeus run ` + terminal.Yellow("-m") + ` subdomain ` + terminal.Yellow("-m") + ` portscan ` + terminal.Yellow("-m") + ` vuln ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Skip heuristics checks") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-t") + ` example.com ` + terminal.Yellow("--heuristics-check") + ` none
` + terminal.Green("# Concurrent targets") + `
osmedeus run ` + terminal.Yellow("-m") + ` recon ` + terminal.Yellow("-T") + ` targets.txt ` + terminal.Yellow("--concurrency") + ` 5
` + terminal.BoldYellow("★ Function Eval (Powerful Scripting)") + `
` + terminal.Green("# Print markdown file") + `
osmedeus func e 'print_markdown_from_file("README.md")'
` + terminal.Green("# Log with variable substitution") + `
osmedeus func e 'log_info("Scanning {{target}}")' ` + terminal.Yellow("-t") + ` example.com
` + terminal.Green("# Save content to file") + `
osmedeus func e 'save_content("data", "/tmp/out.txt")'
` + terminal.Green("# Make HTTP request") + `
osmedeus func e 'httpRequest("https://api.example.com", "GET", {}, "")'
` + terminal.Green("# Sort file using Unix sort") + `
osmedeus func e 'sortUnix("/tmp/input.txt", "/tmp/sorted.txt")'
` + terminal.Green("# Read from stdin") + `
echo 'log_info("hello")' | osmedeus func e -
` + terminal.BoldCyan("▶ Server Examples") + `
` + terminal.Green("# Start server") + `
osmedeus serve
` + terminal.Green("# Custom port") + `
osmedeus serve ` + terminal.Yellow("--port") + ` 8080
` + terminal.Green("# No authentication (dev mode)") + `
osmedeus serve ` + terminal.Yellow("-A") + `
` + terminal.Green("# Distributed master mode") + `
osmedeus serve ` + terminal.Yellow("--master") + `
` + terminal.BoldCyan("▶ Workflow Examples") + `
` + terminal.Green("# List all workflows") + `
osmedeus workflow list
` + terminal.Green("# Show workflow details") + `
osmedeus workflow show recon
` + terminal.Green("# Validate a workflow") + `
osmedeus workflow validate my-workflow
` + terminal.BoldCyan("▶ Worker Examples (Distributed Mode)") + `
` + terminal.Green("# Join worker pool") + `
osmedeus worker join
` + terminal.Green("# With custom Redis URL") + `
osmedeus worker join ` + terminal.Yellow("--redis-url") + ` redis://localhost:6379/0
` + terminal.Green("# Check worker status") + `
osmedeus worker status
` + terminal.BoldCyan("▶ Install Examples") + `
` + terminal.Green("# Install binary") + `
osmedeus install binary ` + terminal.Yellow("--name") + ` nuclei
` + terminal.Green("# Install multiple binaries") + `
osmedeus install binary ` + terminal.Yellow("--name") + ` nuclei ` + terminal.Yellow("--name") + ` httpx
` + terminal.Green("# Install all binaries") + `
osmedeus install binary ` + terminal.Yellow("--all") + `
` + terminal.Green("# Install Nix package manager") + `
osmedeus install binary ` + terminal.Yellow("--nix-installation") + `
` + terminal.Green("# Install binary via Nix") + `
osmedeus install binary ` + terminal.Yellow("--name") + ` nuclei ` + terminal.Yellow("--nix-build-install") + `
` + terminal.Green("# Install all binaries via Nix") + `
osmedeus install binary ` + terminal.Yellow("--all") + ` ` + terminal.Yellow("--nix-build-install") + `
` + terminal.Green("# Install workflows from git") + `
osmedeus install workflow https://github.com/user/workflows.git
` + terminal.BoldCyan("▶ Utility Examples") + `
` + terminal.Green("# Health check") + `
osmedeus health
` + terminal.Green("# Reset config") + `
osmedeus config clean
` + terminal.Green("# Set config value") + `
osmedeus config set server.port 9000
` + terminal.Green("# Database commands") + `
osmedeus db list
osmedeus db seed
osmedeus db clean ` + terminal.Yellow("--force") + `
` + docsFooter()
}
// UsageFullExample returns comprehensive usage with all flags for pager display
func UsageFullExample() string {
return terminal.BoldCyan("═══════════════════════════════════════════════════════════════════") + `
` + terminal.BoldCyan(" OSMEDEUS FULL USAGE REFERENCE") + `
` + terminal.BoldCyan("═══════════════════════════════════════════════════════════════════") + `
` + terminal.BoldYellow("GLOBAL FLAGS") + ` (available for all commands)
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
` + terminal.Yellow("--settings-file") + ` Path to settings file (default: $HOME/osmedeus-base/osm-settings.yaml)
` + terminal.Yellow("-b, --base-folder") + ` Base folder containing workflows and settings
` + terminal.Yellow("-F, --workflow-folder") + ` Custom workflow folder path
` + terminal.Yellow("-v, --verbose") + ` Enable verbose output
` + terminal.Yellow("--debug") + ` Enable debug mode (verbose + debug logging)
` + terminal.Yellow("-q, --silent") + ` Silent mode - suppress all output except errors
` + terminal.Yellow("--log-file") + ` Path to log file (logs to both console and file)
` + terminal.Yellow("--log-file-tmp") + ` Create temporary log file osmedeus-log-<timestamp>.log
` + terminal.Yellow("-H, --usage-example") + ` Show comprehensive usage examples
` + terminal.Yellow("--full-usage-example") + ` Show this full usage reference (pager mode)
` + terminal.Yellow("--spinner") + ` Show spinner animations during execution
` + terminal.Yellow("--disable-logging") + ` Disable all logging output
` + terminal.Yellow("--disable-color") + ` Disable colored output
` + terminal.Yellow("--disable-notification") + ` Disable all notifications
` + terminal.Yellow("--disable-db") + ` Disable database connection (lightweight mode)
` + terminal.Yellow("--ci-output-format") + ` Output results in JSON format for CI pipelines
` + terminal.BoldYellow("RUN COMMAND") + ` - Execute workflows
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus run [flags]
` + terminal.Cyan(" Workflow Selection:") + `
` + terminal.Yellow("-f, --flow") + ` Flow workflow name to execute
` + terminal.Yellow("-m, --module") + ` Module workflow(s) to execute (can specify multiple)
` + terminal.Yellow("--std-module") + ` Read module YAML from stdin
` + terminal.Cyan(" Target Selection:") + `
` + terminal.Yellow("-t, --target") + ` Target(s) to run against (can specify multiple)
` + terminal.Yellow("-T, --target-file") + ` File containing targets (one per line)
` + terminal.Yellow("--empty-target") + ` Run without target (generates placeholder)
` + terminal.Cyan(" Parameters:") + `
` + terminal.Yellow("-p, --params") + ` Additional parameters (key=value format)
` + terminal.Yellow("-P, --params-file") + ` File containing parameters (JSON or YAML)
` + terminal.Yellow("-B, --tactic") + ` Run tactic: aggressive, default, gently
` + terminal.Yellow("--threads-hold") + ` Override thread count (0 = use tactic default)
` + terminal.Cyan(" Execution Control:") + `
` + terminal.Yellow("-c, --concurrency") + ` Number of targets to run concurrently (default: 1)
` + terminal.Yellow("--timeout") + ` Run timeout (e.g., 2h, 3h, 1d)
` + terminal.Yellow("--repeat") + ` Repeat run after completion
` + terminal.Yellow("--repeat-wait-time") + ` Wait time between repeats (default: 1h)
` + terminal.Yellow("--dry-run") + ` Show what would be executed without running
` + terminal.Yellow("-G, --progress-bar") + ` Show progress bar during execution
` + terminal.Cyan(" Workspace:") + `
` + terminal.Yellow("-w, --workspace") + ` Custom workspace path
` + terminal.Yellow("-W, --workspaces-folder") + ` Override {{Workspaces}} variable
` + terminal.Yellow("-S, --space") + ` Override {{TargetSpace}} variable
` + terminal.Cyan(" Filtering:") + `
` + terminal.Yellow("-x, --exclude") + ` Module(s) to exclude from execution
` + terminal.Yellow("--heuristics-check") + ` Heuristics check level: none, basic, advanced
` + terminal.Cyan(" Distributed Mode:") + `
` + terminal.Yellow("-D, --distributed-run") + ` Submit run to distributed worker queue
` + terminal.Yellow("--redis-url") + ` Redis connection URL for distributed mode
` + terminal.BoldYellow("SERVE COMMAND") + ` - Start REST API server
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus serve [flags]
` + terminal.Yellow("--host") + ` Host to bind the server to (default: from config)
` + terminal.Yellow("--port") + ` Port number for the API server
` + terminal.Yellow("-A, --no-auth") + ` Disable authentication (development only)
` + terminal.Yellow("--master") + ` Run as distributed master node
` + terminal.Yellow("--redis-url") + ` Redis connection URL for master mode
` + terminal.BoldYellow("WORKFLOW COMMAND") + ` - Manage workflows
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus workflow list List available workflows
osmedeus workflow show <name> Show workflow details
osmedeus workflow validate <name> Validate a workflow (alias: val)
` + terminal.Cyan(" List Flags:") + `
` + terminal.Yellow("--tags") + ` Filter workflows by tags (comma-separated)
` + terminal.Yellow("--show-tags") + ` Show tags column in output
` + terminal.Cyan(" Show Flags:") + `
` + terminal.Yellow("-v, --verbose") + ` Show detailed variable descriptions
` + terminal.Yellow("--table") + ` Show metadata table instead of YAML
` + terminal.BoldYellow("FUNCTION COMMAND") + ` - Execute utility functions
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus func list List all available functions (alias: ls)
osmedeus func eval <script> Evaluate a script (alias: e)
` + terminal.Cyan(" Eval Flags:") + `
` + terminal.Yellow("-e, --eval") + ` Script to evaluate
` + terminal.Yellow("-t, --target") + ` Target value for {{target}} variable
` + terminal.Yellow("--params") + ` Additional parameters (key=value format)
` + terminal.Yellow("--stdin") + ` Read script from stdin
` + terminal.BoldYellow("WORKER COMMAND") + ` - Distributed worker management
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus worker join Join the distributed worker pool
osmedeus worker status Show worker pool status
` + terminal.Cyan(" Join Flags:") + `
` + terminal.Yellow("--redis-url") + ` Redis connection URL
` + terminal.Yellow("--workers") + ` Number of concurrent workers (default: 5)
` + terminal.Cyan(" Status Flags:") + `
` + terminal.Yellow("--redis-url") + ` Redis connection URL
` + terminal.BoldYellow("DATABASE COMMAND") + ` - Database management
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus db list List tables with row counts (alias: ls)
osmedeus db list -t <table> List records from a table
osmedeus db seed Seed database with sample data
osmedeus db clean --force Remove all data from database
osmedeus db migrate Run database migrations
osmedeus db index workflow Index workflows from filesystem to database
` + terminal.Cyan(" List Flags:") + `
` + terminal.Yellow("-t, --table") + ` Table name to list records from
` + terminal.Yellow("--offset") + ` Number of records to skip (default: 0)
` + terminal.Yellow("--limit") + ` Maximum records to return (default: 50)
` + terminal.Yellow("--json") + ` Output records as JSON only (bypasses TUI)
` + terminal.Yellow("--no-tui") + ` Disable interactive TUI mode, use plain text
` + terminal.Yellow("--where") + ` Filter records (key=value, can be repeated)
` + terminal.Yellow("--columns") + ` Comma-separated columns to display
` + terminal.Yellow("--search") + ` Search all columns for substring
` + terminal.Yellow("--width") + ` Max column width for table display (default: 30)
` + terminal.Yellow("--all") + ` Show all columns including hidden ones
` + terminal.Cyan(" Clean Flags:") + `
` + terminal.Yellow("--force") + ` Skip confirmation prompt
` + terminal.Cyan(" Index Workflow Flags:") + `
` + terminal.Yellow("--force") + ` Force re-index all workflows regardless of checksum
` + terminal.BoldYellow("CONFIG COMMAND") + ` - Configuration management
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus config clean Reset configuration to defaults
osmedeus config set <key> <value> Set a configuration value
osmedeus config view <key> View a configuration value
osmedeus config list List configuration values
` + terminal.BoldYellow("INSTALL COMMAND") + ` - Install components
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus install workflow <source> Install workflows from git/zip
osmedeus install base <source> Install base folder
osmedeus install binary Install binaries from registry
osmedeus install validate Check and fix environment health (alias: val)
osmedeus install env Display environment paths
` + terminal.Cyan(" Binary Flags:") + `
` + terminal.Yellow("-n, --name") + ` Binary name(s) to install (can be repeated)
` + terminal.Yellow("--all") + ` Install all binaries from registry
` + terminal.Yellow("--check") + ` Check if binaries are installed
` + terminal.Yellow("-r, --registry") + ` Custom registry JSON file path or URL
` + terminal.Yellow("--nix-pkgs") + ` Nix package(s) to add (repeatable)
` + terminal.Yellow("--nix-build-install") + ` Use Nix to install binaries instead of direct downloads
` + terminal.Yellow("--nix-installation") + ` Install Nix package manager (Determinate Systems installer)
` + terminal.BoldYellow("HEALTH COMMAND") + ` - Environment health check
` + terminal.Gray("───────────────────────────────────────────────────────────────────") + `
osmedeus health Check environment for issues
` + terminal.BoldCyan("═══════════════════════════════════════════════════════════════════") + `
` + docsFooter()
}
// docsFooter returns the documentation footer
func docsFooter() string {
return terminal.HiCyan("📖 Documentation: ") + terminal.HiWhite(core.DOCS) + "\n"
}
+177
View File
@@ -0,0 +1,177 @@
package cli
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/distributed"
"github.com/j3ssie/osmedeus/v5/internal/terminal"
"github.com/spf13/cobra"
)
var printer = terminal.NewPrinter()
var redisURL string
// workerCmd represents the worker command
var workerCmd = &cobra.Command{
Use: "worker",
Short: "Worker node commands for distributed scanning",
Long: UsageWorker(),
}
// workerJoinCmd joins the worker pool
var workerJoinCmd = &cobra.Command{
Use: "join",
Short: "Join as a worker node",
Long: UsageWorkerJoin(),
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Get()
if cfg == nil {
return errConfigNotLoaded
}
// Override Redis config from URL if provided
if redisURL != "" {
redisCfg, err := distributed.ParseRedisURL(redisURL)
if err != nil {
return err
}
cfg.Redis = *redisCfg
}
// Check Redis is configured
if !cfg.IsRedisConfigured() {
return errRedisNotConfigured
}
// Create worker
worker, err := distributed.NewWorker(cfg)
if err != nil {
return err
}
// Setup graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
cancel()
}()
// Run worker
return worker.Run(ctx)
},
}
// workerStatusCmd shows worker status
var workerStatusCmd = &cobra.Command{
Use: "status",
Short: "Show worker pool status",
Long: UsageWorkerStatus(),
RunE: func(cmd *cobra.Command, args []string) error {
cfg := config.Get()
if cfg == nil {
return errConfigNotLoaded
}
// Override Redis config from URL if provided
if redisURL != "" {
redisCfg, err := distributed.ParseRedisURL(redisURL)
if err != nil {
return err
}
cfg.Redis = *redisCfg
}
// Check Redis is configured
if !cfg.IsRedisConfigured() {
return errRedisNotConfigured
}
// Create master client to query workers
master, err := distributed.NewMaster(cfg)
if err != nil {
return err
}
ctx := context.Background()
workers, err := master.ListWorkers(ctx)
if err != nil {
return err
}
if len(workers) == 0 {
printer.Info("No workers connected")
return nil
}
// Print workers
printer.Section("Connected Workers")
headers := []string{"ID", "Hostname", "Status", "Tasks Done", "Tasks Failed", "Last Heartbeat"}
var rows [][]string
for _, w := range workers {
rows = append(rows, []string{
w.ID,
w.Hostname,
w.Status,
formatInt(w.TasksComplete),
formatInt(w.TasksFailed),
formatHeartbeat(w.LastHeartbeat),
})
}
printMarkdownTable(headers, rows)
return nil
},
}
var errConfigNotLoaded = &exitError{message: "configuration not loaded", code: 1}
var errRedisNotConfigured = &exitError{message: "redis not configured. Add redis section to osm-settings.yaml or use --redis-url", code: 1}
type exitError struct {
message string
code int
}
func (e *exitError) Error() string {
return e.message
}
func init() {
workerJoinCmd.Flags().StringVar(&redisURL, "redis-url", "", "Redis connection URL (overrides settings)")
workerStatusCmd.Flags().StringVar(&redisURL, "redis-url", "", "Redis connection URL (overrides settings)")
workerCmd.AddCommand(workerJoinCmd)
workerCmd.AddCommand(workerStatusCmd)
}
// formatInt formats an integer for display
func formatInt(n int) string {
return fmt.Sprintf("%d", n)
}
// formatHeartbeat formats a time as a relative duration
func formatHeartbeat(t time.Time) string {
if t.IsZero() {
return "never"
}
d := time.Since(t)
if d < time.Minute {
return fmt.Sprintf("%ds ago", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm ago", int(d.Minutes()))
}
return fmt.Sprintf("%dh ago", int(d.Hours()))
}
+1333
View File
File diff suppressed because it is too large Load Diff