mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-17 21:25:49 +02:00
- Refactor table display using tablewriter with box-drawing characters and proper Unicode width handling - Add --as-cron flag to register cron schedules instead of executing immediately - Implement new 'osmedeus assets' command for asset querying with filtering and statistics - Enhance workflow parameter display with categorization and per-module summaries - Improve column width handling and parameter documentation
588 lines
17 KiB
Go
588 lines
17 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/j3ssie/osmedeus/v5/internal/client"
|
|
"github.com/j3ssie/osmedeus/v5/internal/terminal"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
// Client connection flags
|
|
clientRemoteURL string
|
|
clientAuthKey string
|
|
|
|
// Fetch subcommand flags
|
|
clientFetchTable string
|
|
clientFetchWorkspace string
|
|
clientFetchSeverity string
|
|
clientFetchStatus string
|
|
clientFetchLimit int
|
|
clientFetchOffset int
|
|
clientFetchRefresh string
|
|
|
|
// Run subcommand flags
|
|
clientRunFlow string
|
|
clientRunModule string
|
|
clientRunTarget string
|
|
clientRunCancel string
|
|
|
|
// Exec subcommand flags
|
|
clientExecScript string
|
|
clientExecTarget string
|
|
)
|
|
|
|
// clientCmd is the parent command for interacting with a remote osmedeus server
|
|
var clientCmd = &cobra.Command{
|
|
Use: "client",
|
|
Short: "Interact with a remote osmedeus server",
|
|
Long: UsageClient(),
|
|
}
|
|
|
|
// clientFetchCmd fetches data from the server
|
|
var clientFetchCmd = &cobra.Command{
|
|
Use: "fetch",
|
|
Short: "Fetch data from remote server (assets, runs, vulns, etc.)",
|
|
Long: UsageClientFetch(),
|
|
RunE: runClientFetch,
|
|
}
|
|
|
|
// clientRunCmd creates or cancels runs on the server
|
|
var clientRunCmd = &cobra.Command{
|
|
Use: "run",
|
|
Short: "Create or cancel a run on remote server",
|
|
Long: UsageClientRun(),
|
|
RunE: runClientRun,
|
|
}
|
|
|
|
// clientExecCmd executes a function remotely
|
|
var clientExecCmd = &cobra.Command{
|
|
Use: "exec [script]",
|
|
Short: "Execute a function on remote server",
|
|
Long: UsageClientExec(),
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: runClientExec,
|
|
}
|
|
|
|
func init() {
|
|
// Add persistent flags for client connection (available to all subcommands)
|
|
clientCmd.PersistentFlags().StringVar(&clientRemoteURL, "remote-url", "", "Remote server URL (default: $OSM_REMOTE_URL)")
|
|
clientCmd.PersistentFlags().StringVar(&clientAuthKey, "auth-key", "", "API authentication key (default: $OSM_REMOTE_AUTH_KEY)")
|
|
|
|
// Fetch subcommand flags
|
|
clientFetchCmd.Flags().StringVarP(&clientFetchTable, "table", "t", "assets", "Table to fetch data from: runs, step_results, artifacts, assets, event_logs, schedules, workspaces, vulnerabilities, asset_diffs, vuln_diffs")
|
|
clientFetchCmd.Flags().StringVarP(&clientFetchWorkspace, "workspace", "w", "", "Filter by workspace name")
|
|
clientFetchCmd.Flags().StringVar(&clientFetchSeverity, "severity", "", "Filter by severity (for vulnerabilities only)")
|
|
clientFetchCmd.Flags().StringVar(&clientFetchStatus, "status", "", "Filter by status (for runs only)")
|
|
clientFetchCmd.Flags().IntVar(&clientFetchLimit, "limit", 20, "Maximum records to return")
|
|
clientFetchCmd.Flags().IntVar(&clientFetchOffset, "offset", 0, "Number of records to skip (pagination)")
|
|
clientFetchCmd.Flags().StringVar(&clientFetchRefresh, "refresh", "", "auto-refresh interval (e.g., 5s, 1m, 30s)")
|
|
|
|
// Run subcommand flags
|
|
clientRunCmd.Flags().StringVarP(&clientRunFlow, "flow", "f", "", "Flow workflow name")
|
|
clientRunCmd.Flags().StringVarP(&clientRunModule, "module", "m", "", "Module workflow name")
|
|
clientRunCmd.Flags().StringVarP(&clientRunTarget, "target", "T", "", "Target to run against")
|
|
clientRunCmd.Flags().StringVar(&clientRunCancel, "cancel", "", "Run ID to cancel (switches to cancel mode)")
|
|
|
|
// Exec subcommand flags
|
|
clientExecCmd.Flags().StringVarP(&clientExecScript, "script", "s", "", "Script to execute (alternative to positional arg)")
|
|
clientExecCmd.Flags().StringVarP(&clientExecTarget, "target", "t", "", "Target variable for script")
|
|
|
|
// Build command hierarchy
|
|
clientCmd.AddCommand(clientFetchCmd)
|
|
clientCmd.AddCommand(clientRunCmd)
|
|
clientCmd.AddCommand(clientExecCmd)
|
|
}
|
|
|
|
// createClient creates a new API client with the configured URL and auth key
|
|
func createClient() (*client.Client, error) {
|
|
return client.NewClient(clientRemoteURL, clientAuthKey)
|
|
}
|
|
|
|
// outputJSON outputs data as JSON to stdout
|
|
func outputJSON(data interface{}) error {
|
|
output, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal JSON: %w", err)
|
|
}
|
|
fmt.Println(string(output))
|
|
return nil
|
|
}
|
|
|
|
// printClientTable prints data using the same tablewriter renderer as `db ls`
|
|
func printClientTable(tableName string, data interface{}, columns []string, pagination client.Pagination, recordCount int) {
|
|
printer := terminal.NewPrinter()
|
|
printer.Info("Table: %s", tableName)
|
|
|
|
// Calculate pagination info
|
|
startRecord := pagination.Offset + 1
|
|
endRecord := pagination.Offset + recordCount
|
|
if pagination.Total == 0 {
|
|
startRecord = 0
|
|
}
|
|
fmt.Printf("Showing records %d-%d of %d\n\n", startRecord, endRecord, pagination.Total)
|
|
|
|
renderTableWithTablewriter(tableName, data, columns, globalWidth, false, nil)
|
|
}
|
|
|
|
// validFetchTables lists all valid table names for the fetch command
|
|
var validFetchTables = []string{
|
|
"runs", "step_results", "artifacts", "assets", "event_logs",
|
|
"schedules", "workspaces", "vulnerabilities", "asset_diffs", "vuln_diffs",
|
|
}
|
|
|
|
// clientTableDefaultColumns defines default columns for each table in client fetch output
|
|
var clientTableDefaultColumns = map[string][]string{
|
|
"runs": {"run_uuid", "workflow_name", "target", "status", "completed_steps", "total_steps"},
|
|
"step_results": {"step_name", "step_type", "status", "duration_ms", "command"},
|
|
"artifacts": {"name", "artifact_path", "artifact_type", "size_bytes"},
|
|
"assets": {"asset_value", "url", "status_code", "content_length", "title"},
|
|
"event_logs": {"topic", "source", "processed", "workspace"},
|
|
"schedules": {"name", "workflow_name", "trigger_type", "schedule", "is_enabled"},
|
|
"workspaces": {"name", "data_source", "total_assets"},
|
|
"vulnerabilities": {"vuln_title", "severity", "asset_value", "workspace"},
|
|
"asset_diffs": {"workspace_name", "from_time", "to_time", "total_added", "total_removed"},
|
|
"vuln_diffs": {"workspace_name", "from_time", "to_time", "total_added", "total_removed"},
|
|
}
|
|
|
|
// runClientFetchRefreshLoop continuously refreshes the fetch display at the specified interval
|
|
func runClientFetchRefreshLoop(cmd *cobra.Command, args []string, interval time.Duration) error {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
defer signal.Stop(sigChan)
|
|
|
|
for {
|
|
fmt.Print("\033[2J\033[H") // Clear screen
|
|
if err := runClientFetchOnce(cmd, args); err != nil {
|
|
terminal.NewPrinter().Error("Fetch failed: %s", err)
|
|
}
|
|
fmt.Printf("\n%s Refreshing every %s. Press Ctrl+C to stop.\n", terminal.Gray("⟳"), interval)
|
|
|
|
select {
|
|
case <-ticker.C:
|
|
continue
|
|
case <-sigChan:
|
|
fmt.Print("\033[2J\033[H")
|
|
terminal.NewPrinter().Info("Refresh stopped")
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// runClientFetch handles the fetch subcommand
|
|
func runClientFetch(cmd *cobra.Command, args []string) error {
|
|
// Check if refresh mode is enabled
|
|
if clientFetchRefresh != "" {
|
|
interval, err := time.ParseDuration(clientFetchRefresh)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid refresh interval: %w", err)
|
|
}
|
|
if interval < time.Second {
|
|
return fmt.Errorf("refresh interval must be at least 1s")
|
|
}
|
|
return runClientFetchRefreshLoop(cmd, args, interval)
|
|
}
|
|
return runClientFetchOnce(cmd, args)
|
|
}
|
|
|
|
// runClientFetchOnce performs a single fetch and displays the results
|
|
func runClientFetchOnce(cmd *cobra.Command, args []string) error {
|
|
c, err := createClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
query := url.Values{}
|
|
query.Set("limit", strconv.Itoa(clientFetchLimit))
|
|
query.Set("offset", strconv.Itoa(clientFetchOffset))
|
|
|
|
var result interface{}
|
|
var endpoint string
|
|
|
|
// Validate table name
|
|
validTable := false
|
|
for _, t := range validFetchTables {
|
|
if clientFetchTable == t {
|
|
validTable = true
|
|
break
|
|
}
|
|
}
|
|
if !validTable {
|
|
return fmt.Errorf("invalid table: %s (valid tables: %s)", clientFetchTable, strings.Join(validFetchTables, ", "))
|
|
}
|
|
|
|
switch clientFetchTable {
|
|
case "runs":
|
|
endpoint = "/runs"
|
|
if clientFetchStatus != "" {
|
|
query.Set("status", clientFetchStatus)
|
|
}
|
|
var resp client.RunsResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printRunsTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "step_results":
|
|
endpoint = "/step-results"
|
|
if clientFetchWorkspace != "" {
|
|
query.Set("workspace", clientFetchWorkspace)
|
|
}
|
|
if clientFetchStatus != "" {
|
|
query.Set("status", clientFetchStatus)
|
|
}
|
|
var resp client.StepResultsResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printStepResultsTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "artifacts":
|
|
endpoint = "/artifacts"
|
|
if clientFetchWorkspace != "" {
|
|
query.Set("workspace", clientFetchWorkspace)
|
|
}
|
|
var resp client.ArtifactsResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printArtifactsTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "assets":
|
|
endpoint = "/assets"
|
|
if clientFetchWorkspace != "" {
|
|
query.Set("workspace", clientFetchWorkspace)
|
|
}
|
|
var resp client.AssetsResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printAssetsTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "event_logs":
|
|
endpoint = "/event-logs"
|
|
if clientFetchWorkspace != "" {
|
|
query.Set("workspace", clientFetchWorkspace)
|
|
}
|
|
var resp client.EventLogsResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printEventLogsTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "schedules":
|
|
endpoint = "/schedules"
|
|
var resp client.SchedulesResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printSchedulesTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "workspaces":
|
|
endpoint = "/workspaces"
|
|
var resp client.WorkspacesResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printWorkspacesTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "vulnerabilities":
|
|
endpoint = "/vulnerabilities"
|
|
if clientFetchWorkspace != "" {
|
|
query.Set("workspace", clientFetchWorkspace)
|
|
}
|
|
if clientFetchSeverity != "" {
|
|
query.Set("severity", clientFetchSeverity)
|
|
}
|
|
var resp client.VulnerabilitiesResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printVulnerabilitiesTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "asset_diffs":
|
|
endpoint = "/assets/diffs"
|
|
if clientFetchWorkspace != "" {
|
|
query.Set("workspace", clientFetchWorkspace)
|
|
}
|
|
var resp client.AssetDiffsResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printAssetDiffsTable(resp)
|
|
return nil
|
|
}
|
|
|
|
case "vuln_diffs":
|
|
endpoint = "/vulnerabilities/diffs"
|
|
if clientFetchWorkspace != "" {
|
|
query.Set("workspace", clientFetchWorkspace)
|
|
}
|
|
var resp client.VulnDiffsResponse
|
|
if err := c.Get(ctx, endpoint, query, &resp); err != nil {
|
|
return err
|
|
}
|
|
result = resp
|
|
if !globalJSON {
|
|
printVulnDiffsTable(resp)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return outputJSON(result)
|
|
}
|
|
|
|
// runClientRun handles the run subcommand (create or cancel)
|
|
func runClientRun(cmd *cobra.Command, args []string) error {
|
|
c, err := createClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Cancel mode: --cancel flag is set
|
|
if clientRunCancel != "" {
|
|
var resp client.CancelRunResponse
|
|
if err := c.Delete(ctx, "/runs/"+clientRunCancel, &resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
if globalJSON {
|
|
return outputJSON(resp)
|
|
}
|
|
|
|
printer := terminal.NewPrinter()
|
|
printer.Success("Run cancelled: %s", resp.RunUUID)
|
|
return nil
|
|
}
|
|
|
|
// Create mode: create a new run
|
|
if clientRunFlow == "" && clientRunModule == "" {
|
|
return fmt.Errorf("either --flow or --module is required (or use --cancel to cancel a run)")
|
|
}
|
|
if clientRunFlow != "" && clientRunModule != "" {
|
|
return fmt.Errorf("only one of --flow or --module can be specified")
|
|
}
|
|
if clientRunTarget == "" {
|
|
return fmt.Errorf("--target is required")
|
|
}
|
|
|
|
req := client.CreateRunRequest{
|
|
Flow: clientRunFlow,
|
|
Module: clientRunModule,
|
|
Target: clientRunTarget,
|
|
}
|
|
|
|
var resp client.CreateRunResponse
|
|
if err := c.Post(ctx, "/runs", req, &resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
if globalJSON {
|
|
return outputJSON(resp)
|
|
}
|
|
|
|
// Print human-readable output
|
|
printer := terminal.NewPrinter()
|
|
printer.Success("Run created successfully")
|
|
fmt.Printf(" Workflow: %s (%s)\n", resp.Workflow, resp.Kind)
|
|
fmt.Printf(" Target: %s\n", resp.Target)
|
|
fmt.Printf(" Job ID: %s\n", resp.JobID)
|
|
if resp.RunUUID != "" {
|
|
fmt.Printf(" Run UUID: %s\n", resp.RunUUID)
|
|
}
|
|
fmt.Printf(" Status: %s\n", resp.Status)
|
|
fmt.Printf(" Poll URL: %s\n", resp.PollURL)
|
|
|
|
return nil
|
|
}
|
|
|
|
// runClientExec handles the exec subcommand
|
|
func runClientExec(cmd *cobra.Command, args []string) error {
|
|
// Get script from args or flag
|
|
script := clientExecScript
|
|
if len(args) > 0 {
|
|
script = args[0]
|
|
}
|
|
if script == "" {
|
|
return fmt.Errorf("script is required (as argument or via --script)")
|
|
}
|
|
|
|
c, err := createClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
req := client.FunctionEvalRequest{
|
|
Script: script,
|
|
Target: clientExecTarget,
|
|
}
|
|
|
|
var resp client.FunctionEvalResponse
|
|
if err := c.Post(ctx, "/functions/eval", req, &resp); err != nil {
|
|
return err
|
|
}
|
|
|
|
if globalJSON {
|
|
return outputJSON(resp)
|
|
}
|
|
|
|
// Print human-readable output
|
|
if resp.Error {
|
|
return fmt.Errorf("execution failed: %s", resp.Message)
|
|
}
|
|
|
|
// Print result
|
|
switch v := resp.Result.(type) {
|
|
case string:
|
|
fmt.Println(v)
|
|
case nil:
|
|
// No output
|
|
default:
|
|
output, _ := json.MarshalIndent(v, "", " ")
|
|
fmt.Println(string(output))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// printAssetsTable prints assets in a table format
|
|
func printAssetsTable(resp client.AssetsResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No assets found")
|
|
return
|
|
}
|
|
printClientTable("assets", resp.Data, clientTableDefaultColumns["assets"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printWorkspacesTable prints workspaces in a table format
|
|
func printWorkspacesTable(resp client.WorkspacesResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No workspaces found")
|
|
return
|
|
}
|
|
printClientTable("workspaces", resp.Data, clientTableDefaultColumns["workspaces"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printVulnerabilitiesTable prints vulnerabilities in a table format
|
|
func printVulnerabilitiesTable(resp client.VulnerabilitiesResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No vulnerabilities found")
|
|
return
|
|
}
|
|
printClientTable("vulnerabilities", resp.Data, clientTableDefaultColumns["vulnerabilities"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printRunsTable prints runs in a table format
|
|
func printRunsTable(resp client.RunsResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No runs found")
|
|
return
|
|
}
|
|
printClientTable("runs", resp.Data, clientTableDefaultColumns["runs"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printStepResultsTable prints step results in a table format
|
|
func printStepResultsTable(resp client.StepResultsResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No step results found")
|
|
return
|
|
}
|
|
printClientTable("step_results", resp.Data, clientTableDefaultColumns["step_results"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printArtifactsTable prints artifacts in a table format
|
|
func printArtifactsTable(resp client.ArtifactsResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No artifacts found")
|
|
return
|
|
}
|
|
printClientTable("artifacts", resp.Data, clientTableDefaultColumns["artifacts"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printEventLogsTable prints event logs in a table format
|
|
func printEventLogsTable(resp client.EventLogsResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No event logs found")
|
|
return
|
|
}
|
|
printClientTable("event_logs", resp.Data, clientTableDefaultColumns["event_logs"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printSchedulesTable prints schedules in a table format
|
|
func printSchedulesTable(resp client.SchedulesResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No schedules found")
|
|
return
|
|
}
|
|
printClientTable("schedules", resp.Data, clientTableDefaultColumns["schedules"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printAssetDiffsTable prints asset diffs in a table format
|
|
func printAssetDiffsTable(resp client.AssetDiffsResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No asset diffs found")
|
|
return
|
|
}
|
|
printClientTable("asset_diffs", resp.Data, clientTableDefaultColumns["asset_diffs"], resp.Pagination, len(resp.Data))
|
|
}
|
|
|
|
// printVulnDiffsTable prints vulnerability diffs in a table format
|
|
func printVulnDiffsTable(resp client.VulnDiffsResponse) {
|
|
if len(resp.Data) == 0 {
|
|
terminal.NewPrinter().Info("No vulnerability diffs found")
|
|
return
|
|
}
|
|
printClientTable("vuln_diffs", resp.Data, clientTableDefaultColumns["vuln_diffs"], resp.Pagination, len(resp.Data))
|
|
}
|