mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-23 16:12:29 +02:00
- Add query subcommands (vulns, runs, steps) with JSON output and flexible filtering - Enhance assets command with --where, --search, --value filters (fuzzy matching) - Expand asset searchable columns (status_code, content_type, title, dns_records, tls, tech) - Add run status and run cancel subcommands with process termination - Support control-plane and PID-based cancellation with database updates - Implement fuzzyFilters in GetTableRecords for case-insensitive substring matching
206 lines
5.9 KiB
Go
206 lines
5.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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 (
|
|
assetsWorkspace string
|
|
assetsSource string
|
|
assetsType string
|
|
assetsStats bool
|
|
assetsLimit int
|
|
assetsOffset int
|
|
assetsColumns string
|
|
assetsExcludeColumns string
|
|
assetsAll bool
|
|
assetsWhere []string
|
|
assetsSearch string
|
|
assetsValue string
|
|
)
|
|
|
|
var assetsCmd = &cobra.Command{
|
|
Use: "assets [search]",
|
|
Aliases: []string{"asset"},
|
|
Short: "Query and list discovered assets",
|
|
Long: UsageAssets(),
|
|
RunE: runAssets,
|
|
}
|
|
|
|
func init() {
|
|
assetsCmd.Flags().StringVarP(&assetsWorkspace, "workspace", "w", "", "filter by workspace name")
|
|
assetsCmd.Flags().StringVar(&assetsSource, "source", "", "filter by source (fuzzy match, e.g., httpx)")
|
|
assetsCmd.Flags().StringVar(&assetsType, "type", "", "filter by asset_type (fuzzy match, e.g., web, subdomain)")
|
|
assetsCmd.Flags().BoolVar(&assetsStats, "stats", false, "show asset statistics (unique technologies, sources, remarks, types)")
|
|
assetsCmd.Flags().IntVar(&assetsLimit, "limit", 50, "maximum number of records to return")
|
|
assetsCmd.Flags().IntVar(&assetsOffset, "offset", 0, "number of records to skip (for pagination)")
|
|
assetsCmd.Flags().StringVar(&assetsColumns, "columns", "", "comma-separated columns to display")
|
|
assetsCmd.Flags().StringVar(&assetsExcludeColumns, "exclude-columns", "", "comma-separated columns to exclude from output")
|
|
assetsCmd.Flags().BoolVar(&assetsAll, "all", false, "show all columns including hidden ones (id, timestamps)")
|
|
assetsCmd.Flags().StringArrayVar(&assetsWhere, "where", nil, "filter by column (key=value, fuzzy match, repeatable)")
|
|
assetsCmd.Flags().StringVar(&assetsSearch, "search", "", "search all columns for substring (case-insensitive)")
|
|
assetsCmd.Flags().StringVarP(&assetsValue, "value", "V", "", "filter by asset_value (fuzzy match)")
|
|
}
|
|
|
|
func runAssets(cmd *cobra.Command, args []string) error {
|
|
if disableDB {
|
|
return fmt.Errorf("assets command unavailable: --disable-db flag is set")
|
|
}
|
|
|
|
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()
|
|
if err := database.Migrate(ctx); err != nil {
|
|
return fmt.Errorf("failed to run migrations: %w", err)
|
|
}
|
|
|
|
if assetsStats {
|
|
return runAssetsStats(ctx)
|
|
}
|
|
return runAssetsList(ctx, args)
|
|
}
|
|
|
|
func runAssetsList(ctx context.Context, args []string) error {
|
|
// Validate pagination
|
|
if assetsLimit <= 0 {
|
|
assetsLimit = 50
|
|
}
|
|
if assetsLimit > 10000 {
|
|
assetsLimit = 10000
|
|
}
|
|
if assetsOffset < 0 {
|
|
assetsOffset = 0
|
|
}
|
|
|
|
// Exact-match filters
|
|
filters := make(map[string]string)
|
|
if assetsWorkspace != "" {
|
|
filters["workspace"] = assetsWorkspace
|
|
}
|
|
|
|
// Fuzzy-match filters (LIKE)
|
|
fuzzyFilters := parseWhereFilters(assetsWhere)
|
|
if assetsSource != "" {
|
|
fuzzyFilters["source"] = assetsSource
|
|
}
|
|
if assetsType != "" {
|
|
fuzzyFilters["asset_type"] = assetsType
|
|
}
|
|
if assetsValue != "" {
|
|
fuzzyFilters["asset_value"] = assetsValue
|
|
}
|
|
|
|
// Search term: --search flag or positional arg
|
|
search := assetsSearch
|
|
if len(args) > 0 {
|
|
search = args[0]
|
|
}
|
|
|
|
records, err := database.GetTableRecords(ctx, "assets", assetsOffset, assetsLimit, filters, fuzzyFilters, search, database.AssetHeavyColumns)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get assets: %w", err)
|
|
}
|
|
|
|
// JSON output
|
|
if globalJSON {
|
|
jsonBytes, err := json.Marshal(records.Records)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to format records: %w", err)
|
|
}
|
|
fmt.Println(string(jsonBytes))
|
|
return nil
|
|
}
|
|
|
|
// Table output
|
|
printer := terminal.NewPrinter()
|
|
|
|
requestedColumns := parseColumns(assetsColumns)
|
|
columns := getEffectiveColumns("assets", requestedColumns, assetsAll)
|
|
excludeColumns := parseExcludeColumns(assetsExcludeColumns)
|
|
hideDefaultColumns := !assetsAll && len(requestedColumns) == 0 && tableDefaultColumns["assets"] == nil
|
|
|
|
startRecord := records.Offset + 1
|
|
endRecord := records.Offset + assetsLimit
|
|
if endRecord > records.TotalCount {
|
|
endRecord = records.TotalCount
|
|
}
|
|
if records.TotalCount == 0 {
|
|
startRecord = 0
|
|
}
|
|
|
|
printer.Info("Assets")
|
|
fmt.Printf("Showing records %d-%d of %d\n\n", startRecord, endRecord, records.TotalCount)
|
|
|
|
renderTableWithTablewriter("assets", records.Records, columns, globalWidth, hideDefaultColumns, excludeColumns)
|
|
|
|
if records.TotalCount > endRecord {
|
|
nextOffset := records.Offset + records.Limit
|
|
printer.Info("Next page: osmedeus assets --offset %d --limit %d", nextOffset, assetsLimit)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runAssetsStats(ctx context.Context) error {
|
|
stats, err := database.GetAssetStats(ctx, assetsWorkspace)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get asset stats: %w", err)
|
|
}
|
|
|
|
// JSON output
|
|
if globalJSON {
|
|
jsonBytes, err := json.Marshal(stats)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to format stats: %w", err)
|
|
}
|
|
fmt.Println(string(jsonBytes))
|
|
return nil
|
|
}
|
|
|
|
// Table output
|
|
printer := terminal.NewPrinter()
|
|
if assetsWorkspace != "" {
|
|
printer.Info("Asset Statistics (workspace: %s)", assetsWorkspace)
|
|
} else {
|
|
printer.Info("Asset Statistics")
|
|
}
|
|
fmt.Println()
|
|
|
|
printStatCategory("Technologies", stats.Technologies)
|
|
printStatCategory("Sources", stats.Sources)
|
|
printStatCategory("Remarks", stats.Remarks)
|
|
printStatCategory("Asset Types", stats.AssetTypes)
|
|
|
|
return nil
|
|
}
|
|
|
|
func printStatCategory(name string, items []string) {
|
|
fmt.Printf("%s (%d):\n", terminal.Bold(name), len(items))
|
|
if len(items) == 0 {
|
|
fmt.Println(" (none)")
|
|
} else {
|
|
for _, item := range items {
|
|
fmt.Printf(" %s %s\n", terminal.SymbolBullet, strings.TrimSpace(item))
|
|
}
|
|
}
|
|
fmt.Println()
|
|
}
|