mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-03 17:21:59 +08:00
### What problem does this PR solve? issue #13714 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
165 lines
5.2 KiB
Go
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]
|
|
}
|