项目文件夹

文件

241 行
7.0 KiB
Go

package services
import (
"bytes"
"context"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
"clawreef/internal/config"
)
type SkillScannerClient interface {
ScanArchive(ctx context.Context, fileName string, content []byte, options map[string]string) (string, map[string]interface{}, string, error)
AvailableAnalyzers(ctx context.Context) ([]string, error)
}
type noopSkillScannerClient struct{}
func (n *noopSkillScannerClient) ScanArchive(ctx context.Context, fileName string, content []byte, options map[string]string) (string, map[string]interface{}, string, error) {
return "", nil, "", fmt.Errorf("skill scanner is disabled")
}
func (n *noopSkillScannerClient) AvailableAnalyzers(ctx context.Context) ([]string, error) {
return nil, fmt.Errorf("skill scanner is disabled")
}
type httpSkillScannerClient struct {
baseURL string
apiKey string
client *http.Client
}
func NewSkillScannerClient(cfg config.SkillScannerConfig) SkillScannerClient {
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
return &noopSkillScannerClient{}
}
timeout := time.Duration(cfg.TimeoutSeconds) * time.Second
if timeout <= 0 {
timeout = 30 * time.Second
}
return &httpSkillScannerClient{
baseURL: strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/"),
apiKey: strings.TrimSpace(cfg.APIKey),
client: &http.Client{Timeout: timeout},
}
}
func (c *httpSkillScannerClient) ScanArchive(ctx context.Context, fileName string, content []byte, options map[string]string) (string, map[string]interface{}, string, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
part, err := writer.CreateFormFile("file", fileName)
if err != nil {
return "", nil, "", fmt.Errorf("failed to create skill scanner upload: %w", err)
}
if _, err := part.Write(content); err != nil {
return "", nil, "", fmt.Errorf("failed to write skill scanner upload: %w", err)
}
_ = writer.WriteField("format", "json")
if err := writer.Close(); err != nil {
return "", nil, "", fmt.Errorf("failed to finalize skill scanner upload: %w", err)
}
endpoint, err := url.Parse(c.baseURL + "/scan-upload")
if err != nil {
return "", nil, "", fmt.Errorf("failed to build skill scanner url: %w", err)
}
query := endpoint.Query()
applyScanUploadOptions(query, options)
endpoint.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint.String(), &body)
if err != nil {
return "", nil, "", fmt.Errorf("failed to create skill scanner request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Accept", "application/json")
if c.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+c.apiKey)
}
resp, err := c.client.Do(req)
if err != nil {
return "", nil, "", fmt.Errorf("skill scanner request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return "", nil, "", fmt.Errorf("skill scanner returned status %d", resp.StatusCode)
}
var raw map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return "", nil, "", fmt.Errorf("failed to decode skill scanner response: %w", err)
}
riskLevel := normalizeScannerRiskLevel(raw)
summary := extractScannerSummary(raw)
return riskLevel, raw, summary, nil
}
func (c *httpSkillScannerClient) AvailableAnalyzers(ctx context.Context) ([]string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+"/health", nil)
if err != nil {
return nil, fmt.Errorf("failed to create skill scanner health request: %w", err)
}
req.Header.Set("Accept", "application/json")
if c.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+c.apiKey)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("skill scanner health request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("skill scanner health returned status %d", resp.StatusCode)
}
var raw struct {
Analyzers []string `json:"analyzers_available"`
}
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, fmt.Errorf("failed to decode skill scanner health response: %w", err)
}
result := make([]string, 0, len(raw.Analyzers))
for _, item := range raw.Analyzers {
item = strings.TrimSpace(item)
if item == "" {
continue
}
item = strings.TrimSuffix(item, "_analyzer")
result = append(result, item)
}
return result, nil
}
func applyScanUploadOptions(query url.Values, options map[string]string) {
analyzers := splitAnalyzerOption(options["analyzers"])
if hasAnalyzer(analyzers, "behavioral") {
query.Set("use_behavioral", "true")
}
if hasAnalyzer(analyzers, "llm") || hasAnalyzer(analyzers, "meta") {
query.Set("use_llm", "true")
query.Set("llm_provider", "openai")
}
}
func splitAnalyzerOption(value string) []string {
if strings.TrimSpace(value) == "" {
return nil
}
items := strings.Split(value, ",")
result := make([]string, 0, len(items))
for _, item := range items {
item = strings.ToLower(strings.TrimSpace(item))
if item == "" {
continue
}
result = append(result, item)
}
return result
}
func hasAnalyzer(items []string, target string) bool {
target = strings.ToLower(strings.TrimSpace(target))
for _, item := range items {
if item == target {
return true
}
}
return false
}
func normalizeScannerRiskLevel(raw map[string]interface{}) string {
if safe, ok := readBool(raw["is_safe"]); ok && safe {
return skillRiskNone
}
candidates := []string{
readString(raw["risk_level"]),
readString(raw["severity"]),
readString(raw["verdict"]),
readString(raw["max_severity"]),
}
if result, ok := raw["result"].(map[string]interface{}); ok {
if safe, ok := readBool(result["is_safe"]); ok && safe {
return skillRiskNone
}
candidates = append(candidates,
readString(result["risk_level"]),
readString(result["severity"]),
readString(result["verdict"]),
readString(result["max_severity"]),
)
}
for _, value := range candidates {
switch strings.ToLower(strings.TrimSpace(value)) {
case "critical", "high":
return skillRiskHigh
case "medium", "moderate":
return skillRiskMedium
case "low", "warning":
return skillRiskLow
case "none", "clean", "safe", "pass", "info", "informational":
return skillRiskNone
}
}
return skillRiskUnknown
}
func extractScannerSummary(raw map[string]interface{}) string {
candidates := []string{
readString(raw["summary"]),
readString(raw["message"]),
}
if result, ok := raw["result"].(map[string]interface{}); ok {
candidates = append(candidates, readString(result["summary"]), readString(result["message"]))
}
for _, candidate := range candidates {
if strings.TrimSpace(candidate) != "" {
return candidate
}
}
return "Skill scanned by external skill-scanner service"
}
func readString(value interface{}) string {
if text, ok := value.(string); ok {
return text
}
return ""
}
func readBool(value interface{}) (bool, bool) {
boolean, ok := value.(bool)
return boolean, ok
}