Merge pull request #276 from yokowu/feat-security-detail

feat: 提供给插件的安全详情接口
This commit is contained in:
Yoko
2025-08-15 10:29:42 +08:00
committed by GitHub
7 changed files with 86 additions and 6615 deletions

View File

@@ -65,6 +65,7 @@ jobs:
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
defaults:
run:
working-directory: ./backend

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@ type ProxyUsecase interface {
Report(ctx context.Context, req *ReportReq) error
CreateSecurityScanning(ctx context.Context, req *CreateSecurityScanningReq) (string, error)
ListSecurityScanning(ctx context.Context, req *ListSecurityScanningReq) (*ListSecurityScanningBriefResp, error)
ListSecurityDetail(ctx context.Context, req *ListSecurityScanningDetailReq) (*ListSecurityScanningDetailResp, error)
}
type ProxyRepo interface {

View File

@@ -3,7 +3,6 @@ package domain
import (
"context"
"path"
"sort"
"github.com/GoYoko/web"
"github.com/google/uuid"
@@ -25,6 +24,7 @@ type SecurityScanningRepo interface {
Create(ctx context.Context, req CreateSecurityScanningReq) (string, error)
Update(ctx context.Context, id string, fileMap map[string]string, status consts.SecurityScanningStatus, result *scan.Result) error
List(ctx context.Context, req ListSecurityScanningReq) (*ListSecurityScanningResp, error)
ListDetail(ctx context.Context, req ListSecurityScanningDetailReq) (*ListSecurityScanningDetailResp, error)
Detail(ctx context.Context, userID, id string) ([]*SecurityScanningRiskDetail, error)
ListBrief(ctx context.Context, req ListSecurityScanningReq) (*ListSecurityScanningBriefResp, error)
AllRunning(ctx context.Context) ([]*db.SecurityScanning, error)
@@ -39,6 +39,12 @@ type ListSecurityScanningReq struct {
ProjectName string `json:"project_name" query:"project_name"` // 项目名称
}
type ListSecurityScanningDetailReq struct {
web.Pagination
ID string `json:"id" query:"id"` // 扫描任务id
UserID string `json:"-"`
}
type ListSecurityScanningResp struct {
*db.PageInfo
@@ -51,7 +57,14 @@ type ListSecurityScanningBriefResp struct {
Items []*SecurityScanningBrief `json:"items"`
}
type ListSecurityScanningDetailResp struct {
*db.PageInfo
Items []*SecurityScanningRiskDetail `json:"items"`
}
type SecurityScanningBrief struct {
ID string `json:"id"` // 扫描任务id
Workspace string `json:"workspace"` // 项目目录
Status consts.SecurityScanningStatus `json:"status"` // 扫描状态
ReportURL string `json:"report_url"` // 报告url
@@ -63,6 +76,7 @@ func (s *SecurityScanningBrief) From(e *db.SecurityScanning) *SecurityScanningBr
return s
}
s.ID = e.ID.String()
s.Status = e.Status
s.Workspace = e.Workspace
s.CreatedAt = e.CreatedAt.Unix()
@@ -131,31 +145,6 @@ type SecurityScanningRiskDetail struct {
Content string `json:"content"` // 代码内容
}
func (s *SecurityScanningRiskDetail) GetRiskLevelPriority() int {
switch s.Level {
case consts.SecurityScanningRiskLevelSevere:
return 1 // 严重 - 最高优先级
case consts.SecurityScanningRiskLevelCritical:
return 2 // 高危 - 中等优先级
case consts.SecurityScanningRiskLevelSuggest:
return 3 // 建议 - 最低优先级
default:
return 4 // 未知等级放在最后
}
}
type ByRiskLevel []*SecurityScanningRiskDetail
func (a ByRiskLevel) Len() int { return len(a) }
func (a ByRiskLevel) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByRiskLevel) Less(i, j int) bool {
return a[i].GetRiskLevelPriority() < a[j].GetRiskLevelPriority()
}
func SortRiskDetailsByLevel(details []*SecurityScanningRiskDetail) {
sort.Sort(ByRiskLevel(details))
}
func (s *SecurityScanningRiskDetail) From(e *db.SecurityScanningResult) *SecurityScanningRiskDetail {
if e == nil {
return s

View File

@@ -61,7 +61,8 @@ func NewV1Handler(
g.POST("/completions", web.BaseHandler(h.Completions), active.Active("apikey"))
g.POST("/embeddings", web.BaseHandler(h.Embeddings), active.Active("apikey"))
g.POST("/security/scanning", web.BindHandler(h.CreateSecurityScanning), active.Active("apikey"))
g.GET("/security/scanning", web.BindHandler(h.ListSecurityScanning), active.Active("apikey"))
g.GET("/security/scanning", web.BindHandler(h.ListSecurityScanning, web.WithPage()), active.Active("apikey"))
g.GET("/security/scanning/detail", web.BindHandler(h.ListSecurityScanningDetail, web.WithPage()), active.Active("apikey"))
return h
}
@@ -244,7 +245,7 @@ func (h *V1Handler) CreateSecurityScanning(c *web.Context, req domain.CreateSecu
//
// @Tags OpenAIV1
// @Summary 扫描任务列表
// @Description 分页逻辑只支持用 next_token
// @Description 扫描任务列表
// @ID list-security-scanning
// @Accept json
// @Produce json
@@ -253,6 +254,7 @@ func (h *V1Handler) CreateSecurityScanning(c *web.Context, req domain.CreateSecu
// @Router /v1/security/scanning [get]
func (h *V1Handler) ListSecurityScanning(c *web.Context, req domain.ListSecurityScanningReq) error {
req.UserID = middleware.GetApiKey(c).UserID
req.Pagination = *c.Page()
s, err := h.uuse.GetSetting(c.Request().Context())
if err != nil {
return err
@@ -264,3 +266,25 @@ func (h *V1Handler) ListSecurityScanning(c *web.Context, req domain.ListSecurity
}
return c.Success(resp)
}
// ListSecurityScanningDetail 获取扫描任务风险详情列表
//
// @Tags OpenAIV1
// @Summary 获取扫描任务风险详情列表
// @Description 分页只支持 next_token; 首页传空后续判断has_next_page是否为true传入回包给的next_token
// @ID list-security-scanning-detail
// @Accept json
// @Produce json
// @Param id query domain.ListSecurityScanningDetailReq true "扫描任务ID"
// @Success 200 {object} web.Resp{data=domain.ListSecurityScanningDetailResp}
// @Router /v1/security/scanning/detail [get]
func (h *V1Handler) ListSecurityScanningDetail(c *web.Context, req domain.ListSecurityScanningDetailReq) error {
req.Pagination = *c.Page()
user := middleware.GetApiKey(c)
req.UserID = user.ID
resp, err := h.proxyUse.ListSecurityDetail(c.Request().Context(), &req)
if err != nil {
return err
}
return c.Success(resp)
}

View File

@@ -189,3 +189,7 @@ func (p *ProxyUsecase) TaskHandle(ctx context.Context, task *queuerunner.Task[do
func (p *ProxyUsecase) ListSecurityScanning(ctx context.Context, req *domain.ListSecurityScanningReq) (*domain.ListSecurityScanningBriefResp, error) {
return p.securityRepo.ListBrief(ctx, *req)
}
func (p *ProxyUsecase) ListSecurityDetail(ctx context.Context, req *domain.ListSecurityScanningDetailReq) (*domain.ListSecurityScanningDetailResp, error) {
return p.securityRepo.ListDetail(ctx, *req)
}

View File

@@ -216,7 +216,11 @@ func (s *SecurityScanningRepo) Detail(ctx context.Context, userID, id string) ([
}
q := s.db.SecurityScanningResult.Query().
Where(securityscanningresult.SecurityScanningID(sid))
Where(securityscanningresult.SecurityScanningID(sid)).
Order(
BySeverityOrder(),
securityscanningresult.ByCreatedAt(sql.OrderDesc()),
)
if userID != "" {
uid, err := uuid.Parse(userID)
@@ -236,7 +240,6 @@ func (s *SecurityScanningRepo) Detail(ctx context.Context, userID, id string) ([
rs := cvt.Iter(scannings, func(_ int, r *db.SecurityScanningResult) *domain.SecurityScanningRiskDetail {
return cvt.From(r, &domain.SecurityScanningRiskDetail{})
})
domain.SortRiskDetailsByLevel(rs)
return rs, nil
}
@@ -308,3 +311,37 @@ func (s *SecurityScanningRepo) PageWorkspaceFiles(ctx context.Context, id string
return nil
}
func (s *SecurityScanningRepo) ListDetail(ctx context.Context, req domain.ListSecurityScanningDetailReq) (*domain.ListSecurityScanningDetailResp, error) {
sid, err := uuid.Parse(req.ID)
if err != nil {
return nil, err
}
q := s.db.SecurityScanningResult.Query().
Where(securityscanningresult.SecurityScanningID(sid)).
Order(
BySeverityOrder(),
securityscanningresult.ByCreatedAt(sql.OrderDesc()),
securityscanningresult.ByID(sql.OrderDesc()),
)
rs, p, err := q.Page(ctx, req.Page, req.Size)
if err != nil {
return nil, err
}
return &domain.ListSecurityScanningDetailResp{
PageInfo: p,
Items: cvt.Iter(rs, func(_ int, r *db.SecurityScanningResult) *domain.SecurityScanningRiskDetail {
return cvt.From(r, &domain.SecurityScanningRiskDetail{})
}),
}, nil
}
func BySeverityOrder() func(s *sql.Selector) {
return func(s *sql.Selector) {
s.OrderExprFunc(func(b *sql.Builder) {
b.WriteString("case when severity = 'CRITICAL' then 5 when severity = 'ERROR' then 4 when severity = 'WARNING' then 3 when severity = 'INFO' then 2 else 1 end desc")
})
}
}