diff --git a/frontend/types/target.types.ts b/frontend/types/target.types.ts index d9ad36ad..fa6c5c02 100644 --- a/frontend/types/target.types.ts +++ b/frontend/types/target.types.ts @@ -33,6 +33,8 @@ export interface TargetDetail extends Target { websites: number endpoints: number ips: number + directories: number + screenshots: number vulnerabilities: { total: number critical: number diff --git a/go-backend/internal/dto/target.go b/go-backend/internal/dto/target.go index 6799c3a3..61d1608f 100644 --- a/go-backend/internal/dto/target.go +++ b/go-backend/internal/dto/target.go @@ -69,3 +69,34 @@ type BulkDeleteRequest struct { type BulkDeleteResponse struct { DeletedCount int64 `json:"deletedCount"` } + +// TargetDetailResponse represents target detail response with summary +type TargetDetailResponse struct { + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + CreatedAt time.Time `json:"createdAt"` + LastScannedAt *time.Time `json:"lastScannedAt"` + Organizations []OrganizationBrief `json:"organizations,omitempty"` + Summary *TargetSummary `json:"summary"` +} + +// TargetSummary represents target asset statistics +type TargetSummary struct { + Subdomains int64 `json:"subdomains"` + Websites int64 `json:"websites"` + Endpoints int64 `json:"endpoints"` + IPs int64 `json:"ips"` + Directories int64 `json:"directories"` + Screenshots int64 `json:"screenshots"` + Vulnerabilities *VulnerabilitySummary `json:"vulnerabilities"` +} + +// VulnerabilitySummary represents vulnerability statistics by severity +type VulnerabilitySummary struct { + Total int64 `json:"total"` + Critical int64 `json:"critical"` + High int64 `json:"high"` + Medium int64 `json:"medium"` + Low int64 `json:"low"` +} diff --git a/go-backend/internal/handler/target.go b/go-backend/internal/handler/target.go index 16292ea5..30168455 100644 --- a/go-backend/internal/handler/target.go +++ b/go-backend/internal/handler/target.go @@ -97,7 +97,7 @@ func (h *TargetHandler) GetByID(c *gin.Context) { return } - target, err := h.svc.GetByID(id) + target, summary, err := h.svc.GetDetailByID(id) if err != nil { if errors.Is(err, service.ErrTargetNotFound) { dto.NotFound(c, "Target not found") @@ -107,12 +107,13 @@ func (h *TargetHandler) GetByID(c *gin.Context) { return } - dto.Success(c, dto.TargetResponse{ + dto.Success(c, dto.TargetDetailResponse{ ID: target.ID, Name: target.Name, Type: target.Type, CreatedAt: target.CreatedAt, LastScannedAt: target.LastScannedAt, + Summary: summary, }) } diff --git a/go-backend/internal/repository/target.go b/go-backend/internal/repository/target.go index 6eac6c1d..25eb23a5 100644 --- a/go-backend/internal/repository/target.go +++ b/go-backend/internal/repository/target.go @@ -141,3 +141,114 @@ func (r *TargetRepository) FindByNames(names []string) ([]model.Target, error) { Find(&targets).Error return targets, err } + +// TargetAssetCounts holds asset count statistics for a target +type TargetAssetCounts struct { + Subdomains int64 + Websites int64 + Endpoints int64 + IPs int64 + Directories int64 + Screenshots int64 +} + +// VulnerabilityCounts holds vulnerability count statistics by severity +type VulnerabilityCounts struct { + Total int64 + Critical int64 + High int64 + Medium int64 + Low int64 +} + +// GetAssetCounts returns asset counts for a target +func (r *TargetRepository) GetAssetCounts(targetID int) (*TargetAssetCounts, error) { + counts := &TargetAssetCounts{} + + // Count subdomains + if err := r.db.Table("subdomain"). + Where("target_id = ?", targetID). + Count(&counts.Subdomains).Error; err != nil { + return nil, err + } + + // Count websites + if err := r.db.Table("website"). + Where("target_id = ?", targetID). + Count(&counts.Websites).Error; err != nil { + return nil, err + } + + // Count endpoints + if err := r.db.Table("endpoint"). + Where("target_id = ?", targetID). + Count(&counts.Endpoints).Error; err != nil { + return nil, err + } + + // Count distinct IPs from host_port_mapping + if err := r.db.Table("host_port_mapping"). + Where("target_id = ?", targetID). + Select("COUNT(DISTINCT ip)"). + Scan(&counts.IPs).Error; err != nil { + return nil, err + } + + // Count directories + if err := r.db.Table("directory"). + Where("target_id = ?", targetID). + Count(&counts.Directories).Error; err != nil { + return nil, err + } + + // Count screenshots + if err := r.db.Table("screenshot"). + Where("target_id = ?", targetID). + Count(&counts.Screenshots).Error; err != nil { + return nil, err + } + + return counts, nil +} + +// GetVulnerabilityCounts returns vulnerability counts by severity for a target +func (r *TargetRepository) GetVulnerabilityCounts(targetID int) (*VulnerabilityCounts, error) { + counts := &VulnerabilityCounts{} + + // Count total vulnerabilities + if err := r.db.Table("vulnerability"). + Where("target_id = ?", targetID). + Count(&counts.Total).Error; err != nil { + return nil, err + } + + // Count by severity + type severityCount struct { + Severity string + Count int64 + } + var severityCounts []severityCount + + if err := r.db.Table("vulnerability"). + Select("severity, COUNT(*) as count"). + Where("target_id = ?", targetID). + Group("severity"). + Scan(&severityCounts).Error; err != nil { + return nil, err + } + + for _, sc := range severityCounts { + switch sc.Severity { + case "critical": + counts.Critical = sc.Count + case "high": + counts.High = sc.Count + case "medium": + counts.Medium = sc.Count + case "low": + counts.Low = sc.Count + } + } + + return counts, nil +} diff --git a/go-backend/internal/service/target.go b/go-backend/internal/service/target.go index 8a5636e3..ce356b6b 100644 --- a/go-backend/internal/service/target.go +++ b/go-backend/internal/service/target.go @@ -73,6 +73,47 @@ func (s *TargetService) GetByID(id int) (*model.Target, error) { return target, nil } +// GetDetailByID returns a target with asset summary by ID +func (s *TargetService) GetDetailByID(id int) (*model.Target, *dto.TargetSummary, error) { + target, err := s.repo.FindByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, ErrTargetNotFound + } + return nil, nil, err + } + + // Get asset counts + assetCounts, err := s.repo.GetAssetCounts(id) + if err != nil { + return nil, nil, err + } + + // Get vulnerability counts + vulnCounts, err := s.repo.GetVulnerabilityCounts(id) + if err != nil { + return nil, nil, err + } + + summary := &dto.TargetSummary{ + Subdomains: assetCounts.Subdomains, + Websites: assetCounts.Websites, + Endpoints: assetCounts.Endpoints, + IPs: assetCounts.IPs, + Directories: assetCounts.Directories, + Screenshots: assetCounts.Screenshots, + Vulnerabilities: &dto.VulnerabilitySummary{ + Total: vulnCounts.Total, + Critical: vulnCounts.Critical, + High: vulnCounts.High, + Medium: vulnCounts.Medium, + Low: vulnCounts.Low, + }, + } + + return target, summary, nil +} + // Update updates a target func (s *TargetService) Update(id int, req *dto.UpdateTargetRequest) (*model.Target, error) { target, err := s.repo.FindByID(id)