mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-09-13 13:17:47 +02:00
Complete rewrite and re-architecture Osmedeus Engine in v5
This commit is contained in:
+1133
File diff suppressed because it is too large
Load Diff
+739
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+579
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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(¶mFlags, "params", "p", nil, "additional parameters (key=value format)")
|
||||
scanCmd.Flags().StringVarP(¶msFile, "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)")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
`
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user