Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
1328 lines
44 KiB
Go
1328 lines
44 KiB
Go
// internal/pluginmanager/governance.go 在涉及发布风险的操作前评估插件评审、公告、供应链和策略门禁。
|
|
|
|
package pluginmanager
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tursom/mc-gateway/plugin/api"
|
|
)
|
|
|
|
const (
|
|
defaultWarningOverrideTTL = 24 * time.Hour
|
|
)
|
|
|
|
type PreflightAdapter interface {
|
|
RunPreflight(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, profile, action string) (api.PreflightResult, error)
|
|
}
|
|
|
|
type SelfTestAdapter interface {
|
|
RunSelfTest(ctx context.Context, artifact ArtifactRecord, pluginRecord PluginRecord, profile string) (api.SelfTestResult, error)
|
|
}
|
|
|
|
func (m *Manager) EvaluateGovernance(ctx context.Context, pluginID, artifactID, action, profile, configJSON string) (GovernanceDecision, error) {
|
|
decision, _, err := m.evaluateGovernance(ctx, pluginID, artifactID, action, profile, configJSON, false)
|
|
return decision, err
|
|
}
|
|
|
|
func (m *Manager) SetPolicyProfile(profile string) error {
|
|
switch profile {
|
|
case "":
|
|
m.policyProfile = PolicyProfileProd
|
|
case PolicyProfileDev, PolicyProfileStaging, PolicyProfileProd:
|
|
m.policyProfile = profile
|
|
default:
|
|
return fmt.Errorf("invalid policy profile %q", profile)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) PolicyProfile() string {
|
|
return m.currentPolicyProfile()
|
|
}
|
|
|
|
func (m *Manager) currentPolicyProfile() string {
|
|
if m.policyProfile == "" {
|
|
return PolicyProfileProd
|
|
}
|
|
return m.policyProfile
|
|
}
|
|
|
|
func (m *Manager) GovernanceStatus(ctx context.Context, pluginID, artifactID, profile string) (GovernanceStatus, error) {
|
|
plugin, err := m.repo.Plugin(ctx, pluginID)
|
|
if err != nil {
|
|
return GovernanceStatus{}, err
|
|
}
|
|
if artifactID == "" {
|
|
artifactID = plugin.DesiredArtifactID
|
|
}
|
|
decision, conflict, err := m.evaluateGovernance(ctx, pluginID, artifactID, GovernanceActionEnable, profile, plugin.ConfigJSON, true)
|
|
if err != nil {
|
|
return GovernanceStatus{}, err
|
|
}
|
|
reviews, err := m.repo.ListReviews(ctx, pluginID)
|
|
if err != nil {
|
|
return GovernanceStatus{}, err
|
|
}
|
|
overrides, err := m.repo.ListWarningOverrides(ctx, pluginID)
|
|
if err != nil {
|
|
return GovernanceStatus{}, err
|
|
}
|
|
preflights, err := m.repo.ListPreflights(ctx, pluginID)
|
|
if err != nil {
|
|
return GovernanceStatus{}, err
|
|
}
|
|
benchmarks, err := m.repo.ListBenchmarks(ctx, pluginID)
|
|
if err != nil {
|
|
return GovernanceStatus{}, err
|
|
}
|
|
advisories, err := m.repo.ListAdvisories(ctx, pluginID)
|
|
if err != nil {
|
|
return GovernanceStatus{}, err
|
|
}
|
|
return GovernanceStatus{
|
|
Decision: decision,
|
|
Policy: policyForProfile(profile, m.repo.now()),
|
|
Reviews: reviews,
|
|
WarningOverrides: overrides,
|
|
Preflights: preflights,
|
|
Benchmarks: benchmarks,
|
|
Advisories: advisories,
|
|
Conflicts: conflict,
|
|
}, nil
|
|
}
|
|
|
|
func (m *Manager) CreateReview(ctx context.Context, actor, pluginID string, req GovernanceReviewRequest) (ReviewRecord, error) {
|
|
plugin, err := m.repo.Plugin(ctx, pluginID)
|
|
if err != nil {
|
|
return ReviewRecord{}, err
|
|
}
|
|
if req.ArtifactID == "" {
|
|
req.ArtifactID = plugin.DesiredArtifactID
|
|
}
|
|
if req.Profile == "" {
|
|
req.Profile = m.currentPolicyProfile()
|
|
}
|
|
if req.Decision == "" {
|
|
req.Decision = ReviewDecisionApproved
|
|
}
|
|
switch req.Decision {
|
|
case ReviewDecisionApproved, ReviewDecisionRejected:
|
|
default:
|
|
return ReviewRecord{}, fmt.Errorf("invalid review decision %q", req.Decision)
|
|
}
|
|
artifact, manifest, err := m.artifactManifest(ctx, pluginID, req.ArtifactID)
|
|
if err != nil {
|
|
return ReviewRecord{}, err
|
|
}
|
|
fingerprint := governanceFingerprint(plugin, artifact, manifest, policyHash(policyForProfile(req.Profile, m.repo.now())))
|
|
review := ReviewRecord{
|
|
PluginID: pluginID,
|
|
ArtifactID: req.ArtifactID,
|
|
Profile: normalizeProfile(req.Profile),
|
|
RiskLevel: riskLevel(manifest, artifact),
|
|
ConfigHash: fingerprint.ConfigHash,
|
|
ScopeHash: fingerprint.ScopeHash,
|
|
RolloutHash: fingerprint.RolloutHash,
|
|
RuntimeLimitsHash: fingerprint.RuntimeLimitsHash,
|
|
FeaturesHash: fingerprint.FeaturesHash,
|
|
PolicyHash: fingerprint.PolicyHash,
|
|
Decision: req.Decision,
|
|
Notes: req.Notes,
|
|
ReviewedBy: actor,
|
|
}
|
|
review, err = m.repo.SaveReview(ctx, review)
|
|
if err != nil {
|
|
return ReviewRecord{}, err
|
|
}
|
|
_ = m.repo.RecordOperation(ctx, pluginID, req.ArtifactID, "governance_review", "succeeded", actor, "plugin governance review recorded", map[string]any{
|
|
"profile": review.Profile,
|
|
"risk_level": review.RiskLevel,
|
|
"decision": review.Decision,
|
|
"policy_hash": review.PolicyHash,
|
|
})
|
|
return review, nil
|
|
}
|
|
|
|
func (m *Manager) CreateWarningOverride(ctx context.Context, actor, pluginID string, req WarningOverrideRequest) (WarningOverrideRecord, error) {
|
|
plugin, err := m.repo.Plugin(ctx, pluginID)
|
|
if err != nil {
|
|
return WarningOverrideRecord{}, err
|
|
}
|
|
if req.ArtifactID == "" {
|
|
req.ArtifactID = plugin.DesiredArtifactID
|
|
}
|
|
if req.Profile == "" {
|
|
req.Profile = m.currentPolicyProfile()
|
|
}
|
|
if req.Action == "" {
|
|
req.Action = GovernanceActionEnable
|
|
}
|
|
if strings.TrimSpace(req.Reason) == "" {
|
|
return WarningOverrideRecord{}, errors.New("reason is required")
|
|
}
|
|
policy := policyForProfile(req.Profile, m.repo.now())
|
|
if req.TTLSeconds <= 0 || req.TTLSeconds > policy.WarningOverrideTTLSeconds {
|
|
req.TTLSeconds = policy.WarningOverrideTTLSeconds
|
|
}
|
|
decision, err := m.EvaluateGovernance(ctx, pluginID, req.ArtifactID, req.Action, req.Profile, plugin.ConfigJSON)
|
|
if err != nil {
|
|
return WarningOverrideRecord{}, err
|
|
}
|
|
if hasBlockingIssue(decision.Issues) {
|
|
return WarningOverrideRecord{}, errors.New("blocking governance issues cannot be overridden")
|
|
}
|
|
if !hasWarningIssue(decision.Issues) {
|
|
return WarningOverrideRecord{}, errors.New("no warning governance issues require override")
|
|
}
|
|
now := m.repo.now().Unix()
|
|
override := WarningOverrideRecord{
|
|
PluginID: pluginID,
|
|
ArtifactID: req.ArtifactID,
|
|
Profile: normalizeProfile(req.Profile),
|
|
Action: req.Action,
|
|
PolicyHash: decision.PolicyHash,
|
|
Reason: req.Reason,
|
|
CreatedBy: actor,
|
|
ExpiresAt: now + req.TTLSeconds,
|
|
CreatedAt: now,
|
|
}
|
|
override, err = m.repo.SaveWarningOverride(ctx, override)
|
|
if err != nil {
|
|
return WarningOverrideRecord{}, err
|
|
}
|
|
_ = m.repo.RecordOperation(ctx, pluginID, req.ArtifactID, "governance_warning_override", "succeeded", actor, "governance warning override recorded", map[string]any{
|
|
"profile": override.Profile,
|
|
"action": override.Action,
|
|
"policy_hash": override.PolicyHash,
|
|
"expires_at": override.ExpiresAt,
|
|
})
|
|
return override, nil
|
|
}
|
|
|
|
func (m *Manager) RunPreflight(ctx context.Context, actor, pluginID string, req PreflightRequest) (PreflightResult, error) {
|
|
plugin, err := m.repo.Plugin(ctx, pluginID)
|
|
if err != nil {
|
|
return PreflightResult{}, err
|
|
}
|
|
if req.ArtifactID == "" {
|
|
req.ArtifactID = plugin.DesiredArtifactID
|
|
}
|
|
if req.ConfigJSON == "" {
|
|
req.ConfigJSON = plugin.ConfigJSON
|
|
}
|
|
if req.Profile == "" {
|
|
req.Profile = m.currentPolicyProfile()
|
|
}
|
|
if req.Action == "" {
|
|
req.Action = GovernanceActionEnable
|
|
}
|
|
artifact, manifest, err := m.artifactManifest(ctx, pluginID, req.ArtifactID)
|
|
if err != nil {
|
|
return PreflightResult{}, err
|
|
}
|
|
result := m.preflightChecks(ctx, plugin, artifact, manifest, req.Profile, req.Action, req.ConfigJSON)
|
|
if adapter, ok := m.adapter.(PreflightAdapter); ok {
|
|
pluginResult, err := adapter.RunPreflight(ctx, artifact, pluginRecordWithConfig(plugin, artifact.ID, req.ConfigJSON), req.Profile, req.Action)
|
|
result.Checks = append(result.Checks, apiPreflightChecks(pluginResult.Checks)...)
|
|
if err != nil {
|
|
result.Checks = append(result.Checks, PreflightCheck{
|
|
Code: "plugin_preflight_error",
|
|
Severity: GateSeverityBlocking,
|
|
Message: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
result.OK = !preflightHasBlocking(result.Checks)
|
|
result.CreatedAt = m.repo.now().Unix()
|
|
data, _ := json.Marshal(result)
|
|
status := "succeeded"
|
|
if !result.OK {
|
|
status = "failed"
|
|
}
|
|
_, err = m.repo.SavePreflight(ctx, PreflightRecord{
|
|
PluginID: pluginID,
|
|
ArtifactID: req.ArtifactID,
|
|
Profile: normalizeProfile(req.Profile),
|
|
Status: status,
|
|
ResultJSON: string(data),
|
|
CreatedBy: actor,
|
|
})
|
|
if err != nil {
|
|
return PreflightResult{}, err
|
|
}
|
|
_ = m.repo.RecordOperation(ctx, pluginID, req.ArtifactID, "governance_preflight", status, actor, "plugin preflight completed", map[string]any{
|
|
"profile": req.Profile,
|
|
"ok": result.OK,
|
|
})
|
|
return result, nil
|
|
}
|
|
|
|
func (m *Manager) RunSelfTest(ctx context.Context, actor, pluginID string, req SelfTestRequest) (PreflightResult, error) {
|
|
plugin, err := m.repo.Plugin(ctx, pluginID)
|
|
if err != nil {
|
|
return PreflightResult{}, err
|
|
}
|
|
if req.ArtifactID == "" {
|
|
req.ArtifactID = plugin.DesiredArtifactID
|
|
}
|
|
if req.Profile == "" {
|
|
req.Profile = m.currentPolicyProfile()
|
|
}
|
|
artifact, _, err := m.artifactManifest(ctx, pluginID, req.ArtifactID)
|
|
if err != nil {
|
|
return PreflightResult{}, err
|
|
}
|
|
result := PreflightResult{Profile: normalizeProfile(req.Profile)}
|
|
if adapter, ok := m.adapter.(SelfTestAdapter); ok {
|
|
pluginResult, err := adapter.RunSelfTest(ctx, artifact, plugin, req.Profile)
|
|
result.Checks = append(result.Checks, apiPreflightChecks(pluginResult.Checks)...)
|
|
if err != nil {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "plugin_self_test_error", Severity: GateSeverityBlocking, Message: err.Error()})
|
|
}
|
|
} else {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "self_test_not_implemented", Severity: GateSeverityWarning, Message: "plugin does not implement SelfTester"})
|
|
}
|
|
result.OK = !preflightHasBlocking(result.Checks)
|
|
result.CreatedAt = m.repo.now().Unix()
|
|
data, _ := json.Marshal(result)
|
|
status := "succeeded"
|
|
if !result.OK {
|
|
status = "failed"
|
|
}
|
|
_, err = m.repo.SavePreflight(ctx, PreflightRecord{
|
|
PluginID: pluginID,
|
|
ArtifactID: req.ArtifactID,
|
|
Profile: normalizeProfile(req.Profile),
|
|
Status: status,
|
|
ResultJSON: string(data),
|
|
CreatedBy: actor,
|
|
})
|
|
if err != nil {
|
|
return PreflightResult{}, err
|
|
}
|
|
_ = m.repo.RecordOperation(ctx, pluginID, req.ArtifactID, "governance_self_test", status, actor, "plugin self-test completed", map[string]any{
|
|
"profile": req.Profile,
|
|
"ok": result.OK,
|
|
})
|
|
return result, nil
|
|
}
|
|
|
|
func (m *Manager) SaveBenchmark(ctx context.Context, actor string, req BenchmarkRequest) (BenchmarkRecord, error) {
|
|
record, err := m.repo.SaveBenchmark(ctx, actor, req)
|
|
if err != nil {
|
|
return BenchmarkRecord{}, err
|
|
}
|
|
_ = m.repo.RecordOperation(ctx, record.PluginID, record.ArtifactID, "governance_benchmark", "succeeded", actor, "plugin benchmark recorded", map[string]any{
|
|
"profile": record.Profile,
|
|
"benchmark_profile": record.BenchmarkProfile,
|
|
"p95_ms": record.P95MS,
|
|
"p99_ms": record.P99MS,
|
|
"error_rate": record.ErrorRate,
|
|
"active_proxy_limit": record.ActiveProxyCapacity,
|
|
"baseline_diff": record.BaselineDiff,
|
|
})
|
|
return record, nil
|
|
}
|
|
|
|
func (m *Manager) UpsertAdvisory(ctx context.Context, actor string, req AdvisoryRequest) (AdvisoryRecord, error) {
|
|
record, err := m.repo.UpsertAdvisory(ctx, actor, req)
|
|
if err != nil {
|
|
return AdvisoryRecord{}, err
|
|
}
|
|
if record.Action == AdvisoryActionQuarantine || record.Action == AdvisoryActionRevoke {
|
|
m.quarantineAffected(ctx, record)
|
|
}
|
|
targetPlugin := record.PluginID
|
|
if targetPlugin == "" && record.ArtifactSHA256 != "" {
|
|
if artifacts, listErr := m.repo.ListArtifacts(ctx, ""); listErr == nil {
|
|
for _, artifact := range artifacts {
|
|
if artifact.SHA256 == record.ArtifactSHA256 {
|
|
targetPlugin = artifact.PluginID
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ = m.repo.RecordOperation(ctx, targetPlugin, "", "governance_advisory", "succeeded", actor, "plugin advisory upserted", map[string]any{
|
|
"advisory_id": record.AdvisoryID,
|
|
"status": record.Status,
|
|
"action": record.Action,
|
|
"artifact_sha256": record.ArtifactSHA256,
|
|
"plugin_id": record.PluginID,
|
|
"version_range": record.VersionRange,
|
|
"dependency_name": record.DependencyName,
|
|
"dependency_range": record.DependencyRange,
|
|
"recommended_action": record.RecommendedAction,
|
|
"fixed_version": record.FixedVersion,
|
|
"mitigation": record.Mitigation,
|
|
})
|
|
return record, nil
|
|
}
|
|
|
|
func (m *Manager) ListReviews(ctx context.Context, pluginID string) ([]ReviewRecord, error) {
|
|
return m.repo.ListReviews(ctx, pluginID)
|
|
}
|
|
|
|
func (m *Manager) ListWarningOverrides(ctx context.Context, pluginID string) ([]WarningOverrideRecord, error) {
|
|
return m.repo.ListWarningOverrides(ctx, pluginID)
|
|
}
|
|
|
|
func (m *Manager) ListAdvisories(ctx context.Context, pluginID string) ([]AdvisoryRecord, error) {
|
|
return m.repo.ListAdvisories(ctx, pluginID)
|
|
}
|
|
|
|
func (m *Manager) ListPreflights(ctx context.Context, pluginID string) ([]PreflightRecord, error) {
|
|
return m.repo.ListPreflights(ctx, pluginID)
|
|
}
|
|
|
|
func (m *Manager) ListBenchmarks(ctx context.Context, pluginID string) ([]BenchmarkRecord, error) {
|
|
return m.repo.ListBenchmarks(ctx, pluginID)
|
|
}
|
|
|
|
func (m *Manager) evaluateGovernance(ctx context.Context, pluginID, artifactID, action, profile, configJSON string, preview bool) (GovernanceDecision, ConflictAnalysis, error) {
|
|
if action == "" {
|
|
action = GovernanceActionEnable
|
|
}
|
|
profile = normalizeProfile(profile)
|
|
policy := policyForProfile(profile, m.repo.now())
|
|
policyHash := policyHash(policy)
|
|
plugin, err := m.repo.Plugin(ctx, pluginID)
|
|
if err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
}
|
|
if artifactID == "" {
|
|
artifactID = plugin.DesiredArtifactID
|
|
}
|
|
if configJSON == "" {
|
|
configJSON = plugin.ConfigJSON
|
|
}
|
|
artifact, manifest, err := m.artifactManifest(ctx, pluginID, artifactID)
|
|
if err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
}
|
|
now := m.repo.now().Unix()
|
|
decision := GovernanceDecision{
|
|
OK: false,
|
|
Action: action,
|
|
Profile: profile,
|
|
RiskLevel: riskLevel(manifest, artifact),
|
|
PolicyHash: policyHash,
|
|
CreatedAt: now,
|
|
}
|
|
var issues []GovernanceIssue
|
|
if err := m.validateArtifactGate(artifact); err != nil {
|
|
issues = append(issues, issue("artifact_not_loadable", GateSeverityBlocking, err.Error(), pluginID, artifactID, nil))
|
|
}
|
|
preflight := m.preflightChecks(ctx, plugin, artifact, manifest, profile, action, configJSON)
|
|
for _, check := range preflight.Checks {
|
|
issues = append(issues, issue(check.Code, check.Severity, check.Message, pluginID, artifactID, check.Details))
|
|
decision.Checks = append(decision.Checks, issue(check.Code, check.Severity, check.Message, pluginID, artifactID, check.Details))
|
|
}
|
|
advisoryIssues, err := m.advisoryIssues(ctx, artifact, manifest)
|
|
if err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
}
|
|
issues = append(issues, advisoryIssues...)
|
|
supplyChainIssues, err := m.supplyChainGovernanceIssues(ctx, artifact)
|
|
if err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
}
|
|
issues = append(issues, supplyChainIssues...)
|
|
benchmarkIssues, err := m.benchmarkIssues(ctx, artifact, manifest, policy)
|
|
if err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
}
|
|
issues = append(issues, benchmarkIssues...)
|
|
conflict, err := m.conflictAnalysis(ctx, plugin, artifact, manifest)
|
|
if err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
}
|
|
issues = append(issues, conflict.Issues...)
|
|
if reviewRequired(profile, decision.RiskLevel, policy) {
|
|
decision.ReviewRequired = true
|
|
fingerprint := governanceFingerprint(pluginRecordWithConfig(plugin, artifactID, configJSON), artifact, manifest, policyHash)
|
|
reviewOK, err := m.hasMatchingReview(ctx, pluginID, artifactID, profile, fingerprint)
|
|
if err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
}
|
|
if !reviewOK {
|
|
issues = append(issues, issue("review_required", GateSeverityBlocking, "high risk plugin requires approved review for this policy snapshot", pluginID, artifactID, map[string]any{
|
|
"profile": profile,
|
|
"risk_level": decision.RiskLevel,
|
|
"policy_hash": policyHash,
|
|
}))
|
|
}
|
|
}
|
|
decision.Issues = sortedIssues(issues)
|
|
if hasBlockingIssue(decision.Issues) {
|
|
decision.OK = false
|
|
return decision, conflict, nil
|
|
}
|
|
if hasWarningIssue(decision.Issues) && !preview {
|
|
if _, ok, err := m.repo.ActiveWarningOverride(ctx, pluginID, artifactID, profile, action, policyHash); err != nil {
|
|
return GovernanceDecision{}, ConflictAnalysis{}, err
|
|
} else if !ok {
|
|
decision.OK = false
|
|
return decision, conflict, nil
|
|
}
|
|
decision.WarningOverrideUsed = true
|
|
}
|
|
decision.OK = true
|
|
return decision, conflict, nil
|
|
}
|
|
|
|
func (m *Manager) preflightChecks(ctx context.Context, plugin PluginRecord, artifact ArtifactRecord, manifest Manifest, profile, action, configJSON string) PreflightResult {
|
|
result := PreflightResult{Profile: normalizeProfile(profile), CreatedAt: m.repo.now().Unix()}
|
|
if configJSON == "" {
|
|
configJSON = "{}"
|
|
}
|
|
if !json.Valid([]byte(configJSON)) {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "config_invalid", Severity: GateSeverityBlocking, Message: "config_json must be valid JSON"})
|
|
return result
|
|
}
|
|
if err := validateConfigSchema(manifest.ConfigSchema, configJSON); err != nil {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "config_schema_failed", Severity: GateSeverityBlocking, Message: err.Error()})
|
|
}
|
|
if err := m.validateSecretRefs(ctx, manifest, configJSON); err != nil {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "secret_missing", Severity: GateSeverityBlocking, Message: err.Error()})
|
|
}
|
|
if missing := requiredFeatures(manifest); len(missing) > 0 {
|
|
result.Checks = append(result.Checks, PreflightCheck{
|
|
Code: "feature_missing",
|
|
Severity: GateSeverityBlocking,
|
|
Message: "required feature declaration is not supported by gateway",
|
|
Details: map[string]any{"features": missing},
|
|
})
|
|
}
|
|
if artifact.RuntimeType == RuntimeSandbox && m.serviceMode != PluginServiceModeSandboxProcess {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "sandbox_runtime_disabled", Severity: GateSeverityBlocking, Message: "sandbox-process runtime is disabled by plugin service mode"})
|
|
}
|
|
if artifact.RuntimeType == RuntimeWASM && m.serviceMode != PluginServiceModeSandboxProcess {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "wasm_runtime_disabled", Severity: GateSeverityBlocking, Message: "wasm runtime is disabled by plugin service mode"})
|
|
}
|
|
if caps := requiredRuntimeCapabilities(artifact); artifact.RuntimeType == RuntimeSandbox && len(caps) > 0 {
|
|
result.Checks = append(result.Checks, PreflightCheck{
|
|
Code: "capability_enforcement_unavailable",
|
|
Severity: GateSeverityBlocking,
|
|
Message: "sandbox-process required capabilities cannot be enforced by this gateway",
|
|
Details: map[string]any{"capabilities": caps},
|
|
})
|
|
}
|
|
if manifest.RuntimeLimits.HandlerTimeoutMS > int(DefaultHandlerTimeout.Milliseconds()) {
|
|
result.Checks = append(result.Checks, PreflightCheck{
|
|
Code: "runtime_limits_warning",
|
|
Severity: GateSeverityWarning,
|
|
Message: "handler timeout is unset or exceeds default runtime limit",
|
|
Details: map[string]any{
|
|
"handler_timeout_ms": manifest.RuntimeLimits.HandlerTimeoutMS,
|
|
"default_ms": DefaultHandlerTimeout.Milliseconds(),
|
|
},
|
|
})
|
|
}
|
|
scope := manifestScope(manifest)
|
|
if upstreamModeFromArtifact(artifact) == UpstreamModeProtocolProxy && len(scope.Values) == 0 {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "scope_global", Severity: GateSeverityWarning, Message: "plugin scope defaults to global"})
|
|
}
|
|
rollout := manifestRollout(manifest)
|
|
if upstreamModeFromArtifact(artifact) == UpstreamModeProtocolProxy && rollout.Mode == "all" && normalizeProfile(profile) == PolicyProfileProd {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "rollout_all_prod", Severity: GateSeverityWarning, Message: "prod rollout applies to all traffic"})
|
|
}
|
|
if upstreamModeFromArtifact(artifact) == UpstreamModeProtocolProxy {
|
|
var summary CapabilitySummary
|
|
_ = json.Unmarshal([]byte(artifact.CapabilitiesSummaryJSON), &summary)
|
|
if summary.Minecraft == nil {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "minecraft_capability_missing", Severity: GateSeverityBlocking, Message: "protocol-proxy plugins must declare minecraft capability"})
|
|
} else {
|
|
if summary.Minecraft.ProtocolVersions.Min == 0 && summary.Minecraft.ProtocolVersions.Max == 0 && len(summary.Minecraft.ProtocolVersions.Tested) == 0 {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "minecraft_protocol_untested", Severity: GateSeverityWarning, Message: "minecraft capability does not declare tested protocol versions"})
|
|
}
|
|
if len(summary.Minecraft.Forwarding.Supported) == 0 {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "backend_forwarding_warning", Severity: GateSeverityWarning, Message: "backend forwarding behavior is not declared"})
|
|
}
|
|
if summary.Minecraft.Forwarding.RequiresSecret {
|
|
hasSecret := false
|
|
for _, spec := range manifest.Secrets {
|
|
if spec.Required {
|
|
hasSecret = true
|
|
break
|
|
}
|
|
}
|
|
if !hasSecret {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "backend_forwarding_secret_missing", Severity: GateSeverityBlocking, Message: "minecraft forwarding requires a declared required secret"})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if external := externalDependencies(manifest); len(external) > 0 {
|
|
result.Checks = append(result.Checks, PreflightCheck{Code: "external_dependencies_declared", Severity: GateSeverityInfo, Message: "plugin declares external dependencies", Details: map[string]any{"dependencies": external}})
|
|
}
|
|
result.OK = !preflightHasBlocking(result.Checks)
|
|
return result
|
|
}
|
|
|
|
func (m *Manager) conflictAnalysis(ctx context.Context, target PluginRecord, artifact ArtifactRecord, manifest Manifest) (ConflictAnalysis, error) {
|
|
analysis := ConflictAnalysis{CreatedAt: m.repo.now().Unix(), Plan: m.DispatchPlan(ctx)}
|
|
if upstreamModeFromArtifact(artifact) != UpstreamModeProtocolProxy {
|
|
analysis.OK = true
|
|
return analysis, nil
|
|
}
|
|
targetScope := manifestScope(manifest)
|
|
plugins, err := m.repo.ListPlugins(ctx)
|
|
if err != nil {
|
|
return ConflictAnalysis{}, err
|
|
}
|
|
for _, plugin := range plugins {
|
|
if plugin.ID == target.ID || plugin.RuntimeState != RuntimeEnabled || plugin.ActiveArtifactID == "" {
|
|
continue
|
|
}
|
|
otherArtifact, err := m.repo.Artifact(ctx, plugin.ActiveArtifactID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if upstreamModeFromArtifact(otherArtifact) != UpstreamModeProtocolProxy {
|
|
continue
|
|
}
|
|
var otherManifest Manifest
|
|
if json.Unmarshal([]byte(otherArtifact.MetadataJSON), &otherManifest) != nil {
|
|
continue
|
|
}
|
|
otherScope := manifestScope(otherManifest)
|
|
if scopesOverlap(targetScope, otherScope) {
|
|
analysis.Issues = append(analysis.Issues, issue("scope_overlap", GateSeverityBlocking, "protocol-proxy scope overlaps enabled plugin", target.ID, artifact.ID, map[string]any{
|
|
"other_plugin_id": plugin.ID,
|
|
"other_artifact_id": otherArtifact.ID,
|
|
"scope": targetScope.Values,
|
|
}))
|
|
analysis.Issues = append(analysis.Issues, issue("protocol_proxy_singleton", GateSeverityBlocking, "only one protocol-proxy plugin can own an overlapping scope", target.ID, artifact.ID, map[string]any{
|
|
"other_plugin_id": plugin.ID,
|
|
}))
|
|
} else if target.Priority >= plugin.Priority {
|
|
analysis.Issues = append(analysis.Issues, issue("shadowed_handler", GateSeverityWarning, "protocol-proxy handler may be shadowed by a higher priority plugin", target.ID, artifact.ID, map[string]any{
|
|
"other_plugin_id": plugin.ID,
|
|
"other_priority": plugin.Priority,
|
|
"priority": target.Priority,
|
|
}))
|
|
}
|
|
}
|
|
providers := providerSingletons(manifest)
|
|
if len(providers) > 0 {
|
|
for _, plugin := range plugins {
|
|
if plugin.ID == target.ID || plugin.RuntimeState != RuntimeEnabled || plugin.ActiveArtifactID == "" {
|
|
continue
|
|
}
|
|
otherArtifact, err := m.repo.Artifact(ctx, plugin.ActiveArtifactID)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var otherManifest Manifest
|
|
if json.Unmarshal([]byte(otherArtifact.MetadataJSON), &otherManifest) != nil {
|
|
continue
|
|
}
|
|
for _, provider := range intersectStrings(providers, providerSingletons(otherManifest)) {
|
|
analysis.Issues = append(analysis.Issues, issue("provider_singleton", GateSeverityBlocking, "provider singleton is already owned by enabled plugin", target.ID, artifact.ID, map[string]any{
|
|
"provider": provider,
|
|
"other_plugin_id": plugin.ID,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
if middlewareCycle(manifest) {
|
|
analysis.Issues = append(analysis.Issues, issue("middleware_ordering_cycle", GateSeverityBlocking, "middleware ordering declaration contains a cycle", target.ID, artifact.ID, nil))
|
|
}
|
|
analysis.Issues = sortedIssues(analysis.Issues)
|
|
analysis.OK = !hasBlockingIssue(analysis.Issues)
|
|
return analysis, nil
|
|
}
|
|
|
|
func (m *Manager) advisoryIssues(ctx context.Context, artifact ArtifactRecord, manifest Manifest) ([]GovernanceIssue, error) {
|
|
advisories, err := m.repo.ListAdvisories(ctx, artifact.PluginID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var issues []GovernanceIssue
|
|
for _, advisory := range advisories {
|
|
if advisory.Status == AdvisoryStatusAcked || advisory.Status == "" && advisory.Action == AdvisoryActionMitigate {
|
|
continue
|
|
}
|
|
if !advisoryMatches(advisory, artifact, manifest) {
|
|
continue
|
|
}
|
|
severity := GateSeverityWarning
|
|
switch advisory.Action {
|
|
case AdvisoryActionDenylist, AdvisoryActionQuarantine, AdvisoryActionRevoke:
|
|
severity = GateSeverityBlocking
|
|
}
|
|
code := "advisory_" + advisory.Action
|
|
if advisory.Status == AdvisoryStatusRevoked {
|
|
code = "advisory_revoke"
|
|
severity = GateSeverityBlocking
|
|
}
|
|
issues = append(issues, issue(code, severity, "artifact matches local security advisory", artifact.PluginID, artifact.ID, map[string]any{
|
|
"advisory_id": advisory.AdvisoryID,
|
|
"recommended_action": advisory.RecommendedAction,
|
|
"fixed_version": advisory.FixedVersion,
|
|
"mitigation": advisory.Mitigation,
|
|
}))
|
|
}
|
|
return issues, nil
|
|
}
|
|
|
|
func (m *Manager) benchmarkIssues(ctx context.Context, artifact ArtifactRecord, manifest Manifest, policy PolicySnapshot) ([]GovernanceIssue, error) {
|
|
benchmarks, err := m.repo.ListBenchmarks(ctx, artifact.PluginID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var latest *BenchmarkRecord
|
|
for idx := range benchmarks {
|
|
benchmark := benchmarks[idx]
|
|
if benchmark.ArtifactID == artifact.ID && benchmark.Profile == policy.Profile {
|
|
latest = &benchmark
|
|
break
|
|
}
|
|
}
|
|
if latest == nil {
|
|
return nil, nil
|
|
}
|
|
var issues []GovernanceIssue
|
|
if latest.BaselineDiff >= policy.BlockBenchmarkRegression {
|
|
issues = append(issues, issue("benchmark_regression_blocking", GateSeverityBlocking, "benchmark regression exceeds blocking threshold", artifact.PluginID, artifact.ID, map[string]any{"baseline_diff": latest.BaselineDiff}))
|
|
} else if latest.BaselineDiff >= policy.WarnBenchmarkRegression {
|
|
issues = append(issues, issue("benchmark_regression_warning", GateSeverityWarning, "benchmark regression exceeds warning threshold", artifact.PluginID, artifact.ID, map[string]any{"baseline_diff": latest.BaselineDiff}))
|
|
}
|
|
if manifest.RuntimeLimits.HandlerTimeoutMS > 0 && latest.P99MS > float64(manifest.RuntimeLimits.HandlerTimeoutMS) {
|
|
issues = append(issues, issue("benchmark_runtime_limit_exceeded", GateSeverityBlocking, "P99 exceeds handler runtime limit", artifact.PluginID, artifact.ID, map[string]any{
|
|
"p99_ms": latest.P99MS,
|
|
"limit": manifest.RuntimeLimits.HandlerTimeoutMS,
|
|
}))
|
|
}
|
|
if latest.ActiveProxyCapacity > 0 {
|
|
active := m.activeProxyCountLocked(artifact.PluginID)
|
|
if int64(active) > latest.ActiveProxyCapacity {
|
|
issues = append(issues, issue("active_proxy_capacity_exceeded", GateSeverityBlocking, "active proxy connections exceed benchmarked capacity", artifact.PluginID, artifact.ID, map[string]any{
|
|
"active": active,
|
|
"capacity": latest.ActiveProxyCapacity,
|
|
}))
|
|
}
|
|
}
|
|
if latest.ErrorRate >= 0.05 {
|
|
issues = append(issues, issue("benchmark_error_rate_blocking", GateSeverityBlocking, "benchmark error rate exceeds blocking threshold", artifact.PluginID, artifact.ID, map[string]any{"error_rate": latest.ErrorRate}))
|
|
} else if latest.ErrorRate >= 0.01 {
|
|
issues = append(issues, issue("benchmark_error_rate_warning", GateSeverityWarning, "benchmark error rate exceeds warning threshold", artifact.PluginID, artifact.ID, map[string]any{"error_rate": latest.ErrorRate}))
|
|
}
|
|
return issues, nil
|
|
}
|
|
|
|
func (m *Manager) supplyChainGovernanceIssues(ctx context.Context, artifact ArtifactRecord) ([]GovernanceIssue, error) {
|
|
assessments, err := m.repo.ListSupplyChainAssessments(ctx, artifact.PluginID, artifact.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(assessments) == 0 {
|
|
return nil, nil
|
|
}
|
|
latest := assessments[0]
|
|
if latest.Status == SupplyChainStatusAllowed {
|
|
return nil, nil
|
|
}
|
|
issues := append([]GovernanceIssue(nil), latest.Issues...)
|
|
if latest.Status == SupplyChainStatusBlocked && len(issues) == 0 {
|
|
issues = append(issues, issue("supply_chain_blocked", GateSeverityBlocking, "latest supply chain assessment blocks this artifact", artifact.PluginID, artifact.ID, nil))
|
|
}
|
|
return sortedIssues(issues), nil
|
|
}
|
|
|
|
func (m *Manager) hasMatchingReview(ctx context.Context, pluginID, artifactID, profile string, fingerprint governanceFingerprintValue) (bool, error) {
|
|
reviews, err := m.repo.ListReviews(ctx, pluginID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
for _, review := range reviews {
|
|
if review.PluginID != pluginID || review.ArtifactID != artifactID || review.Profile != profile || review.Decision != ReviewDecisionApproved {
|
|
continue
|
|
}
|
|
if review.ConfigHash == fingerprint.ConfigHash &&
|
|
review.ScopeHash == fingerprint.ScopeHash &&
|
|
review.RolloutHash == fingerprint.RolloutHash &&
|
|
review.RuntimeLimitsHash == fingerprint.RuntimeLimitsHash &&
|
|
review.FeaturesHash == fingerprint.FeaturesHash &&
|
|
review.PolicyHash == fingerprint.PolicyHash {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (m *Manager) artifactManifest(ctx context.Context, pluginID, artifactID string) (ArtifactRecord, Manifest, error) {
|
|
artifact, err := m.repo.Artifact(ctx, artifactID)
|
|
if err != nil {
|
|
return ArtifactRecord{}, Manifest{}, err
|
|
}
|
|
if artifact.PluginID != pluginID {
|
|
return ArtifactRecord{}, Manifest{}, errors.New("artifact plugin_id does not match")
|
|
}
|
|
var manifest Manifest
|
|
if err := json.Unmarshal([]byte(artifact.MetadataJSON), &manifest); err != nil {
|
|
return ArtifactRecord{}, Manifest{}, err
|
|
}
|
|
return artifact, manifest, nil
|
|
}
|
|
|
|
type governanceFingerprintValue struct {
|
|
ConfigHash string
|
|
ScopeHash string
|
|
RolloutHash string
|
|
RuntimeLimitsHash string
|
|
FeaturesHash string
|
|
PolicyHash string
|
|
}
|
|
|
|
func governanceFingerprint(plugin PluginRecord, artifact ArtifactRecord, manifest Manifest, policyHash string) governanceFingerprintValue {
|
|
_ = artifact
|
|
return governanceFingerprintValue{
|
|
ConfigHash: stableHashJSONRaw(defaultJSONObject(plugin.ConfigJSON)),
|
|
ScopeHash: stableHash(manifestScope(manifest).Values),
|
|
RolloutHash: stableHash(manifestRollout(manifest)),
|
|
RuntimeLimitsHash: stableHash(manifest.RuntimeLimits),
|
|
FeaturesHash: stableHash(requiredFeatures(manifest)),
|
|
PolicyHash: policyHash,
|
|
}
|
|
}
|
|
|
|
func policyForProfile(profile string, now time.Time) PolicySnapshot {
|
|
profile = normalizeProfile(profile)
|
|
policy := PolicySnapshot{
|
|
Profile: profile,
|
|
WarningOverrideTTLSeconds: int64(defaultWarningOverrideTTL.Seconds()),
|
|
ReviewRequiredRisk: RiskHigh,
|
|
WarnBenchmarkRegression: 0.20,
|
|
BlockBenchmarkRegression: 0.50,
|
|
CreatedAt: now.Unix(),
|
|
}
|
|
switch profile {
|
|
case PolicyProfileDev:
|
|
policy.ReviewRequiredRisk = ""
|
|
policy.WarningOverrideTTLSeconds = int64((7 * 24 * time.Hour).Seconds())
|
|
case PolicyProfileStaging:
|
|
policy.WarningOverrideTTLSeconds = int64((72 * time.Hour).Seconds())
|
|
case PolicyProfileProd:
|
|
policy.WarningOverrideTTLSeconds = int64((24 * time.Hour).Seconds())
|
|
}
|
|
return policy
|
|
}
|
|
|
|
func policyHash(policy PolicySnapshot) string {
|
|
policy.CreatedAt = 0
|
|
return stableHash(policy)
|
|
}
|
|
|
|
func normalizeProfile(profile string) string {
|
|
switch profile {
|
|
case PolicyProfileDev, PolicyProfileStaging, PolicyProfileProd:
|
|
return profile
|
|
case "":
|
|
return PolicyProfileProd
|
|
default:
|
|
return PolicyProfileProd
|
|
}
|
|
}
|
|
|
|
func riskLevel(manifest Manifest, artifact ArtifactRecord) string {
|
|
var caps map[string]any
|
|
_ = json.Unmarshal(manifest.Capabilities, &caps)
|
|
if governance, ok := caps["governance"].(map[string]any); ok {
|
|
if risk, _ := governance["risk_level"].(string); risk != "" {
|
|
switch risk {
|
|
case RiskLow, RiskMedium, RiskHigh:
|
|
return risk
|
|
}
|
|
}
|
|
}
|
|
if upstreamModeFromArtifact(artifact) == UpstreamModeProtocolProxy {
|
|
return RiskHigh
|
|
}
|
|
if len(manifest.Secrets) > 0 || len(externalDependencies(manifest)) > 0 {
|
|
return RiskMedium
|
|
}
|
|
return RiskLow
|
|
}
|
|
|
|
func reviewRequired(profile, risk string, policy PolicySnapshot) bool {
|
|
if profile != PolicyProfileProd || policy.ReviewRequiredRisk == "" {
|
|
return false
|
|
}
|
|
return riskRank(risk) >= riskRank(policy.ReviewRequiredRisk)
|
|
}
|
|
|
|
func riskRank(risk string) int {
|
|
switch risk {
|
|
case RiskHigh:
|
|
return 3
|
|
case RiskMedium:
|
|
return 2
|
|
case RiskLow:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
type scopeSpec struct {
|
|
Type string
|
|
Values []string
|
|
}
|
|
|
|
type rolloutSpec struct {
|
|
Mode string `json:"mode"`
|
|
}
|
|
|
|
func manifestScope(manifest Manifest) scopeSpec {
|
|
var caps map[string]any
|
|
_ = json.Unmarshal(manifest.Capabilities, &caps)
|
|
raw, _ := caps["scope"].(map[string]any)
|
|
spec := scopeSpec{Type: "global"}
|
|
if typ, _ := raw["type"].(string); typ != "" {
|
|
spec.Type = typ
|
|
}
|
|
for _, key := range []string{"values", "hosts", "routes", "listeners"} {
|
|
spec.Values = append(spec.Values, stringSlice(raw[key])...)
|
|
}
|
|
if value, _ := raw["value"].(string); value != "" {
|
|
spec.Values = append(spec.Values, value)
|
|
}
|
|
spec.Values = uniqueSortedStrings(spec.Values)
|
|
return spec
|
|
}
|
|
|
|
func manifestRollout(manifest Manifest) rolloutSpec {
|
|
var caps map[string]any
|
|
_ = json.Unmarshal(manifest.Capabilities, &caps)
|
|
raw, _ := caps["rollout"].(map[string]any)
|
|
mode, _ := raw["mode"].(string)
|
|
if mode == "" {
|
|
mode = "all"
|
|
}
|
|
return rolloutSpec{Mode: mode}
|
|
}
|
|
|
|
func scopesOverlap(a, b scopeSpec) bool {
|
|
if len(a.Values) == 0 || len(b.Values) == 0 || a.Type == "global" || b.Type == "global" {
|
|
return true
|
|
}
|
|
for _, left := range a.Values {
|
|
for _, right := range b.Values {
|
|
if left == right || left == "*" || right == "*" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func requiredFeatures(manifest Manifest) []string {
|
|
var caps map[string]any
|
|
_ = json.Unmarshal(manifest.Capabilities, &caps)
|
|
var features []string
|
|
for _, rawKey := range []string{"required_features", "features"} {
|
|
for _, feature := range stringSlice(caps[rawKey]) {
|
|
if !supportedFeature(feature) {
|
|
features = append(features, feature)
|
|
}
|
|
}
|
|
}
|
|
return uniqueSortedStrings(features)
|
|
}
|
|
|
|
func supportedFeature(feature string) bool {
|
|
switch feature {
|
|
case "", ExtensionUpstreamConnect, ExtensionRouteResolve, ExtensionRouteResolver, ExtensionStatusPing,
|
|
ExtensionConnectionFilter, ExtensionHandshakeFilter, ExtensionEventSubscriber,
|
|
ExtensionProvider, ExtensionAuthProvider, ExtensionAdminAuthProvider,
|
|
"upstream.connect", "minecraft", "config", "secret", "preflight", "self-test":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func externalDependencies(manifest Manifest) []string {
|
|
var caps map[string]any
|
|
_ = json.Unmarshal(manifest.Capabilities, &caps)
|
|
var deps []string
|
|
for _, key := range []string{"external_dependencies", "external_deps", "dependencies"} {
|
|
deps = append(deps, stringSlice(caps[key])...)
|
|
}
|
|
return uniqueSortedStrings(deps)
|
|
}
|
|
|
|
func providerSingletons(manifest Manifest) []string {
|
|
var caps map[string]any
|
|
_ = json.Unmarshal(manifest.Capabilities, &caps)
|
|
var providers []string
|
|
for _, key := range []string{"provider_singletons", "providers"} {
|
|
providers = append(providers, stringSlice(caps[key])...)
|
|
}
|
|
return uniqueSortedStrings(providers)
|
|
}
|
|
|
|
func middlewareCycle(manifest Manifest) bool {
|
|
var caps map[string]any
|
|
_ = json.Unmarshal(manifest.Capabilities, &caps)
|
|
raw, ok := caps["middleware_order"].([]any)
|
|
if !ok || len(raw) == 0 {
|
|
return false
|
|
}
|
|
graph := map[string][]string{}
|
|
for _, item := range raw {
|
|
edge, _ := item.(map[string]any)
|
|
before, _ := edge["before"].(string)
|
|
after, _ := edge["after"].(string)
|
|
if before != "" && after != "" {
|
|
graph[before] = append(graph[before], after)
|
|
}
|
|
}
|
|
visiting := map[string]bool{}
|
|
visited := map[string]bool{}
|
|
var visit func(string) bool
|
|
visit = func(node string) bool {
|
|
if visiting[node] {
|
|
return true
|
|
}
|
|
if visited[node] {
|
|
return false
|
|
}
|
|
visiting[node] = true
|
|
for _, next := range graph[node] {
|
|
if visit(next) {
|
|
return true
|
|
}
|
|
}
|
|
visiting[node] = false
|
|
visited[node] = true
|
|
return false
|
|
}
|
|
for node := range graph {
|
|
if visit(node) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func advisoryMatches(advisory AdvisoryRecord, artifact ArtifactRecord, manifest Manifest) bool {
|
|
if advisory.ArtifactSHA256 != "" && advisory.ArtifactSHA256 == artifact.SHA256 {
|
|
return true
|
|
}
|
|
if advisory.PluginID != "" && advisory.PluginID == artifact.PluginID {
|
|
return versionInRange(artifact.Version, advisory.VersionRange)
|
|
}
|
|
if advisory.DependencyName != "" {
|
|
for _, dep := range sbomDependencies(manifest) {
|
|
if dep.Name == advisory.DependencyName && versionInRange(dep.Version, advisory.DependencyRange) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type dependencySpec struct {
|
|
Name string
|
|
Version string
|
|
}
|
|
|
|
func sbomDependencies(manifest Manifest) []dependencySpec {
|
|
var raw map[string]any
|
|
if len(manifest.SupplyChain) == 0 || json.Unmarshal(manifest.SupplyChain, &raw) != nil {
|
|
return nil
|
|
}
|
|
var deps []dependencySpec
|
|
for _, key := range []string{"dependencies", "sbom_dependencies", "modules"} {
|
|
items, _ := raw[key].([]any)
|
|
for _, item := range items {
|
|
obj, _ := item.(map[string]any)
|
|
name, _ := obj["name"].(string)
|
|
if name == "" {
|
|
name, _ = obj["path"].(string)
|
|
}
|
|
version, _ := obj["version"].(string)
|
|
if name != "" {
|
|
deps = append(deps, dependencySpec{Name: name, Version: version})
|
|
}
|
|
}
|
|
}
|
|
return deps
|
|
}
|
|
|
|
func versionInRange(version, expr string) bool {
|
|
expr = strings.TrimSpace(expr)
|
|
if expr == "" || expr == "*" {
|
|
return true
|
|
}
|
|
for _, part := range strings.Split(expr, ",") {
|
|
part = strings.TrimSpace(part)
|
|
switch {
|
|
case strings.HasPrefix(part, "<="):
|
|
if compareVersion(version, strings.TrimSpace(strings.TrimPrefix(part, "<="))) > 0 {
|
|
return false
|
|
}
|
|
case strings.HasPrefix(part, ">="):
|
|
if compareVersion(version, strings.TrimSpace(strings.TrimPrefix(part, ">="))) < 0 {
|
|
return false
|
|
}
|
|
case strings.HasPrefix(part, "<"):
|
|
if compareVersion(version, strings.TrimSpace(strings.TrimPrefix(part, "<"))) >= 0 {
|
|
return false
|
|
}
|
|
case strings.HasPrefix(part, ">"):
|
|
if compareVersion(version, strings.TrimSpace(strings.TrimPrefix(part, ">"))) <= 0 {
|
|
return false
|
|
}
|
|
default:
|
|
if version != part {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func compareVersion(a, b string) int {
|
|
as := versionParts(a)
|
|
bs := versionParts(b)
|
|
for i := 0; i < len(as) || i < len(bs); i++ {
|
|
var av, bv int
|
|
if i < len(as) {
|
|
av = as[i]
|
|
}
|
|
if i < len(bs) {
|
|
bv = bs[i]
|
|
}
|
|
if av < bv {
|
|
return -1
|
|
}
|
|
if av > bv {
|
|
return 1
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func versionParts(version string) []int {
|
|
version = strings.TrimPrefix(strings.TrimSpace(version), "v")
|
|
fields := strings.FieldsFunc(version, func(r rune) bool {
|
|
return r == '.' || r == '-' || r == '+'
|
|
})
|
|
var parts []int
|
|
for _, field := range fields {
|
|
n, err := strconv.Atoi(field)
|
|
if err != nil {
|
|
break
|
|
}
|
|
parts = append(parts, n)
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func apiPreflightChecks(checks []api.PreflightCheck) []PreflightCheck {
|
|
result := make([]PreflightCheck, 0, len(checks))
|
|
for _, check := range checks {
|
|
severity := check.Severity
|
|
if severity == "" {
|
|
severity = GateSeverityWarning
|
|
}
|
|
result = append(result, PreflightCheck{
|
|
Code: check.Code,
|
|
Severity: severity,
|
|
Message: check.Message,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func pluginRecordWithConfig(plugin PluginRecord, artifactID, configJSON string) PluginRecord {
|
|
plugin.DesiredArtifactID = artifactID
|
|
plugin.ConfigJSON = configJSON
|
|
return plugin
|
|
}
|
|
|
|
func issue(code, severity, message, pluginID, artifactID string, details map[string]any) GovernanceIssue {
|
|
return GovernanceIssue{Code: code, Severity: severity, Message: message, PluginID: pluginID, ArtifactID: artifactID, Details: details}
|
|
}
|
|
|
|
func sortedIssues(issues []GovernanceIssue) []GovernanceIssue {
|
|
sort.SliceStable(issues, func(i, j int) bool {
|
|
if issues[i].Severity != issues[j].Severity {
|
|
return severityRank(issues[i].Severity) > severityRank(issues[j].Severity)
|
|
}
|
|
if issueRank(issues[i].Code) != issueRank(issues[j].Code) {
|
|
return issueRank(issues[i].Code) > issueRank(issues[j].Code)
|
|
}
|
|
if issues[i].Code != issues[j].Code {
|
|
return issues[i].Code < issues[j].Code
|
|
}
|
|
return issues[i].PluginID < issues[j].PluginID
|
|
})
|
|
return issues
|
|
}
|
|
|
|
func issueRank(code string) int {
|
|
switch code {
|
|
case "scope_overlap":
|
|
return 10
|
|
case "review_required":
|
|
return 9
|
|
case "feature_missing", "secret_missing":
|
|
return 8
|
|
case "advisory_revoke":
|
|
return 7
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func severityRank(severity string) int {
|
|
switch severity {
|
|
case GateSeverityBlocking:
|
|
return 3
|
|
case GateSeverityWarning:
|
|
return 2
|
|
case GateSeverityInfo:
|
|
return 1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func hasBlockingIssue(issues []GovernanceIssue) bool {
|
|
for _, issue := range issues {
|
|
if issue.Severity == GateSeverityBlocking {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasWarningIssue(issues []GovernanceIssue) bool {
|
|
for _, issue := range issues {
|
|
if issue.Severity == GateSeverityWarning {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func preflightHasBlocking(checks []PreflightCheck) bool {
|
|
for _, check := range checks {
|
|
if check.Severity == GateSeverityBlocking {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func stableHash(value any) string {
|
|
data, _ := json.Marshal(value)
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func stableHashJSONRaw(raw string) string {
|
|
var value any
|
|
if json.Unmarshal([]byte(defaultJSONObject(raw)), &value) != nil {
|
|
return stableHash(raw)
|
|
}
|
|
return stableHash(value)
|
|
}
|
|
|
|
func stringSlice(value any) []string {
|
|
switch typed := value.(type) {
|
|
case []string:
|
|
return typed
|
|
case []any:
|
|
result := make([]string, 0, len(typed))
|
|
for _, item := range typed {
|
|
if text, ok := item.(string); ok && text != "" {
|
|
result = append(result, text)
|
|
}
|
|
}
|
|
return result
|
|
case string:
|
|
if typed != "" {
|
|
return []string{typed}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func manifestCapabilitiesRaw(artifact ArtifactRecord) string {
|
|
var manifest Manifest
|
|
if json.Unmarshal([]byte(artifact.MetadataJSON), &manifest) != nil {
|
|
return "{}"
|
|
}
|
|
if len(manifest.Capabilities) == 0 {
|
|
return "{}"
|
|
}
|
|
return string(manifest.Capabilities)
|
|
}
|
|
|
|
func jsonObjectFromRaw(raw, key string) any {
|
|
var value map[string]any
|
|
if json.Unmarshal([]byte(defaultJSONObject(raw)), &value) != nil {
|
|
return nil
|
|
}
|
|
return value[key]
|
|
}
|
|
|
|
func uniqueSortedStrings(values []string) []string {
|
|
seen := make(map[string]bool, len(values))
|
|
var result []string
|
|
for _, value := range values {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || seen[value] {
|
|
continue
|
|
}
|
|
seen[value] = true
|
|
result = append(result, value)
|
|
}
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
func intersectStrings(left, right []string) []string {
|
|
set := make(map[string]bool, len(left))
|
|
for _, value := range left {
|
|
set[value] = true
|
|
}
|
|
var result []string
|
|
for _, value := range right {
|
|
if set[value] {
|
|
result = append(result, value)
|
|
}
|
|
}
|
|
return uniqueSortedStrings(result)
|
|
}
|
|
|
|
func governanceBlockedError(decision GovernanceDecision) error {
|
|
for _, issue := range decision.Issues {
|
|
if issue.Severity == GateSeverityBlocking {
|
|
return fmt.Errorf("governance gate blocked: %s: %s", issue.Code, issue.Message)
|
|
}
|
|
}
|
|
for _, issue := range decision.Issues {
|
|
if issue.Severity == GateSeverityWarning {
|
|
return fmt.Errorf("governance warning requires override: %s: %s", issue.Code, issue.Message)
|
|
}
|
|
}
|
|
return errors.New("governance gate blocked")
|
|
}
|