Files
osmedeus/internal/database/stats.go
T
j3ssie 8d413aecb9 feat: table rendering, cron schedules, and assets command
- 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
2026-02-17 15:19:49 +07:00

281 lines
7.1 KiB
Go

package database
import (
"context"
"fmt"
"sort"
"github.com/j3ssie/osmedeus/v5/internal/parser"
"github.com/uptrace/bun"
)
// SystemStats contains aggregated system statistics
type SystemStats struct {
Workflows WorkflowStats `json:"workflows"`
Runs RunStats `json:"runs"`
Workspaces WorkspaceStats `json:"workspaces"`
Assets AssetStats `json:"assets"`
Vulnerabilities VulnerabilityStats `json:"vulnerabilities"`
Schedules ScheduleStats `json:"schedules"`
}
// WorkflowStats contains workflow counts
type WorkflowStats struct {
Total int `json:"total"`
Flows int `json:"flows"`
Modules int `json:"modules"`
}
// RunStats contains run counts by status
type RunStats struct {
Total int `json:"total"`
Completed int `json:"completed"`
Running int `json:"running"`
Failed int `json:"failed"`
Pending int `json:"pending"`
}
// WorkspaceStats contains workspace counts
type WorkspaceStats struct {
Total int `json:"total"`
}
// AssetStats contains asset counts
type AssetStats struct {
Total int `json:"total"`
}
// VulnerabilityStats contains vulnerability counts by severity
type VulnerabilityStats struct {
Total int `json:"total"`
Critical int `json:"critical"`
High int `json:"high"`
Medium int `json:"medium"`
Low int `json:"low"`
}
// ScheduleStats contains schedule counts
type ScheduleStats struct {
Total int `json:"total"`
Enabled int `json:"enabled"`
}
// GetSystemStats retrieves aggregated system statistics from the database and workflows
func GetSystemStats(ctx context.Context, workflowsPath string) (*SystemStats, error) {
db := GetDB()
stats := &SystemStats{}
// Get workflow stats from loader
if workflowsPath != "" {
loader := parser.NewLoader(workflowsPath)
flows, modules, err := loader.ListAllWorkflows()
if err == nil {
stats.Workflows = WorkflowStats{
Total: len(flows) + len(modules),
Flows: len(flows),
Modules: len(modules),
}
}
}
// Get run stats
runStats, err := getRunStats(ctx, db)
if err == nil {
stats.Runs = runStats
}
// Get workspace stats
workspaceCount, err := db.NewSelect().Model((*Workspace)(nil)).Count(ctx)
if err == nil {
stats.Workspaces = WorkspaceStats{Total: workspaceCount}
}
// Get asset stats
assetCount, err := db.NewSelect().Model((*Asset)(nil)).Count(ctx)
if err == nil {
stats.Assets = AssetStats{Total: assetCount}
}
// Get vulnerability stats (aggregated from workspaces)
vulnStats, err := getVulnerabilityStats(ctx, db)
if err == nil {
stats.Vulnerabilities = vulnStats
}
// Get schedule stats
scheduleStats, err := getScheduleStats(ctx, db)
if err == nil {
stats.Schedules = scheduleStats
}
return stats, nil
}
// getRunStats retrieves run counts grouped by status in a single query
func getRunStats(ctx context.Context, db *bun.DB) (RunStats, error) {
var result struct {
Total int `bun:"total"`
Completed int `bun:"completed"`
Running int `bun:"running"`
Failed int `bun:"failed"`
Pending int `bun:"pending"`
}
err := db.NewSelect().
Model((*Run)(nil)).
ColumnExpr("COUNT(*) AS total").
ColumnExpr("SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed").
ColumnExpr("SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) AS running").
ColumnExpr("SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed").
ColumnExpr("SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) AS pending").
Scan(ctx, &result)
if err != nil {
return RunStats{}, err
}
return RunStats{
Total: result.Total,
Completed: result.Completed,
Running: result.Running,
Failed: result.Failed,
Pending: result.Pending,
}, nil
}
// getVulnerabilityStats retrieves aggregated vulnerability counts from workspaces
func getVulnerabilityStats(ctx context.Context, db *bun.DB) (VulnerabilityStats, error) {
stats := VulnerabilityStats{}
var result struct {
Critical int `bun:"critical"`
High int `bun:"high"`
Medium int `bun:"medium"`
Low int `bun:"low"`
}
err := db.NewSelect().
Model((*Workspace)(nil)).
ColumnExpr("COALESCE(SUM(vuln_critical), 0) AS critical").
ColumnExpr("COALESCE(SUM(vuln_high), 0) AS high").
ColumnExpr("COALESCE(SUM(vuln_medium), 0) AS medium").
ColumnExpr("COALESCE(SUM(vuln_low), 0) AS low").
Scan(ctx, &result)
if err != nil {
return stats, err
}
stats.Critical = result.Critical
stats.High = result.High
stats.Medium = result.Medium
stats.Low = result.Low
stats.Total = result.Critical + result.High + result.Medium + result.Low
return stats, nil
}
// getScheduleStats retrieves schedule counts in a single query
func getScheduleStats(ctx context.Context, db *bun.DB) (ScheduleStats, error) {
var result struct {
Total int `bun:"total"`
Enabled int `bun:"enabled"`
}
err := db.NewSelect().
Model((*Schedule)(nil)).
ColumnExpr("COUNT(*) AS total").
ColumnExpr("SUM(CASE WHEN is_enabled = true THEN 1 ELSE 0 END) AS enabled").
Scan(ctx, &result)
if err != nil {
return ScheduleStats{}, err
}
return ScheduleStats{
Total: result.Total,
Enabled: result.Enabled,
}, nil
}
// AssetStatsData contains unique lists of asset metadata
type AssetStatsData struct {
Technologies []string `json:"technologies"`
Sources []string `json:"sources"`
Remarks []string `json:"remarks"`
AssetTypes []string `json:"asset_types"`
}
// GetAssetStats retrieves unique values for technologies, sources, remarks, and asset_types
// with optional workspace filtering
func GetAssetStats(ctx context.Context, workspace string) (*AssetStatsData, error) {
db := GetDB()
// Fetch all assets (optionally filtered by workspace)
var assets []Asset
query := db.NewSelect().
Model(&assets).
Column("technologies", "source", "remarks", "asset_type")
if workspace != "" {
query = query.Where("workspace = ?", workspace)
}
err := query.Scan(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch assets: %w", err)
}
// Deduplicate in Go (simpler than DB-specific JSON functions)
techSet := make(map[string]bool)
sourceSet := make(map[string]bool)
remarkSet := make(map[string]bool)
assetTypeSet := make(map[string]bool)
for _, asset := range assets {
// Technologies (JSON array)
for _, tech := range asset.Technologies {
if tech != "" {
techSet[tech] = true
}
}
// Source (string)
if asset.Source != "" {
sourceSet[asset.Source] = true
}
// Remarks (JSON array)
for _, remark := range asset.Remarks {
if remark != "" {
remarkSet[remark] = true
}
}
// AssetType (string)
if asset.AssetType != "" {
assetTypeSet[asset.AssetType] = true
}
}
// Convert maps to sorted slices
result := &AssetStatsData{
Technologies: mapKeysToSortedSlice(techSet),
Sources: mapKeysToSortedSlice(sourceSet),
Remarks: mapKeysToSortedSlice(remarkSet),
AssetTypes: mapKeysToSortedSlice(assetTypeSet),
}
return result, nil
}
// mapKeysToSortedSlice converts map keys to sorted string slice
func mapKeysToSortedSlice(m map[string]bool) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}