yuan-lab-llm--clawmanager
241 行
7.0 KiB
Go
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
|
|
}
|