Files
ragflow/internal/cli/filesystem/skill_hub/security/guard.go
Yingfeng 4ee0702aed Feat: add skills space to context engine (#13908)
### What problem does this PR solve?

issue #13714

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2026-04-30 12:36:03 +08:00

165 lines
5.2 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package security
import (
"fmt"
"strings"
)
// Guard provides security policy enforcement
type Guard struct {
trustedRepos map[string]bool
policy map[string][3]string
}
// NewGuard creates a new security guard
func NewGuard() *Guard {
return &Guard{
trustedRepos: TrustedRepos,
policy: InstallPolicy,
}
}
// extractCanonicalRepo extracts the canonical owner/repo from an identifier
// Supports formats: "owner/repo", "github.com/owner/repo/path", "owner/repo/path"
func extractCanonicalRepo(identifier string) string {
// Normalize the identifier
identifier = strings.TrimSpace(identifier)
identifier = strings.ToLower(identifier)
// Remove protocol prefix if present
if idx := strings.Index(identifier, "://"); idx != -1 {
identifier = identifier[idx+3:]
}
// Remove github.com prefix if present
if strings.HasPrefix(identifier, "github.com/") {
identifier = strings.TrimPrefix(identifier, "github.com/")
}
// Split into parts
parts := strings.Split(identifier, "/")
if len(parts) < 2 {
return ""
}
// Extract owner and repo (first two components)
owner := strings.TrimSpace(parts[0])
repo := strings.TrimSpace(parts[1])
if owner == "" || repo == "" {
return ""
}
return owner + "/" + repo
}
// ResolveTrustLevel determines the trust level based on source and identifier
func (g *Guard) ResolveTrustLevel(source, identifier string) string {
// Official/builtin source
if source == "official" || source == "builtin" {
return "builtin"
}
// Extract canonical repo key and check against trusted repositories
canonicalRepo := extractCanonicalRepo(identifier)
if canonicalRepo != "" && g.trustedRepos[canonicalRepo] {
return "trusted"
}
// Default to community
return "community"
}
// ShouldAllowInstall determines if installation should be allowed based on scan results
// Returns (allowed bool, reason string)
func (g *Guard) ShouldAllowInstall(result *ScanResult, force bool) (bool, string) {
policy, ok := g.policy[result.TrustLevel]
if !ok {
policy = g.policy["community"]
}
vi, ok := VerdictIndex[result.Verdict]
if !ok {
vi = 2 // dangerous
}
decision := policy[vi]
switch decision {
case "allow":
return true, fmt.Sprintf("Allowed (%s source, %s verdict)", result.TrustLevel, result.Verdict)
case "ask":
return false, fmt.Sprintf("Requires confirmation (%s source + %s verdict, %d findings)",
result.TrustLevel, result.Verdict, len(result.Findings))
case "block":
if force {
return true, fmt.Sprintf("Force-installed despite %s verdict (%d findings)",
result.Verdict, len(result.Findings))
}
return false, fmt.Sprintf("Blocked (%s source + %s verdict, %d findings). Use --force to override.",
result.TrustLevel, result.Verdict, len(result.Findings))
}
return false, "Unknown policy decision"
}
// FormatScanReport formats a scan result for display
func (g *Guard) FormatScanReport(result *ScanResult) string {
var sb strings.Builder
sb.WriteString("╔════════════════════════════════════════════════════════════════╗\n")
sb.WriteString(fmt.Sprintf("║ Security Scan Report: %-40s ║\n", result.SkillName))
sb.WriteString("╚════════════════════════════════════════════════════════════════╝\n")
sb.WriteString(fmt.Sprintf("Source: %s\n", result.Source))
sb.WriteString(fmt.Sprintf("Trust Level: %s\n", result.TrustLevel))
sb.WriteString(fmt.Sprintf("Verdict: %s\n", result.Verdict))
sb.WriteString(fmt.Sprintf("Findings: %d\n", len(result.Findings)))
if len(result.Findings) > 0 {
sb.WriteString("\n─── Findings ───\n")
// Group by severity
severityOrder := []string{"critical", "high", "medium", "low"}
for _, sev := range severityOrder {
for _, f := range result.Findings {
if f.Severity == sev {
sb.WriteString(fmt.Sprintf("\n[%s] %s\n", strings.ToUpper(sev), f.PatternID))
sb.WriteString(fmt.Sprintf(" Category: %s\n", f.Category))
sb.WriteString(fmt.Sprintf(" File: %s:%d\n", f.File, f.Line))
sb.WriteString(fmt.Sprintf(" Match: %s\n", f.Match))
sb.WriteString(fmt.Sprintf(" Description: %s\n", f.Description))
}
}
}
}
sb.WriteString("\n")
return sb.String()
}
// AddTrustedRepo adds a repository to the trusted list
func (g *Guard) AddTrustedRepo(repo string) {
g.trustedRepos[repo] = true
}
// IsTrustedRepo checks if a repository is trusted
func (g *Guard) IsTrustedRepo(repo string) bool {
return g.trustedRepos[repo]
}