diff --git a/connectorkit/manifest.go b/connectorkit/manifest.go new file mode 100644 index 0000000..3d2ef94 --- /dev/null +++ b/connectorkit/manifest.go @@ -0,0 +1,120 @@ +package connectorkit + +import "regexp" + +type Manifest struct { + SchemaVersion string `json:"schemaVersion"` + Slug string `json:"slug"` + Name string `json:"name"` + Version string `json:"version"` + AssetType string `json:"assetType"` + Category string `json:"category"` + Summary string `json:"summary,omitempty"` + Description string `json:"description,omitempty"` + Publisher map[string]string `json:"publisher"` + Repository map[string]string `json:"repository"` + Documentation map[string]string `json:"documentation"` + Capabilities []string `json:"capabilities"` + Evidence map[string]bool `json:"evidence"` + Security map[string]bool `json:"security"` + SupportedLanguages []string `json:"supportedLanguages,omitempty"` +} + +type Finding struct { + Code string + Severity string + Message string +} + +type ValidationResult struct { + OK bool + EvidenceScore int + Tier string + Findings []Finding +} + +var slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,95}$`) +var versionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$`) +var categories = map[string]bool{ + "ai-governance": true, + "compliance": true, + "crm": true, + "devops": true, + "erp": true, + "iam": true, + "security": true, + "storage": true, +} + +func ValidateManifest(manifest Manifest) ValidationResult { + findings := make([]Finding, 0) + if !slugPattern.MatchString(manifest.Slug) { + findings = append(findings, Finding{"manifest.invalid_slug", "error", "slug must be lower-kebab-case"}) + } + if !versionPattern.MatchString(manifest.Version) { + findings = append(findings, Finding{"manifest.invalid_version", "error", "version must be semantic versioning"}) + } + if !categories[manifest.Category] { + findings = append(findings, Finding{"manifest.invalid_category", "error", "unsupported category"}) + } + if len(manifest.Capabilities) == 0 { + findings = append(findings, Finding{"capabilities.empty", "error", "at least one capability is required"}) + } + if manifest.SchemaVersion == "" || manifest.Name == "" || manifest.AssetType == "" { + findings = append(findings, Finding{"manifest.missing_field", "error", "schemaVersion, name, and assetType are required"}) + } + score := 0 + if len(findings) == 0 { + score = 0 + if manifest.Evidence["receipts"] && manifest.Evidence["offlineVerification"] { + score += 25 + } else { + score += 18 + } + if manifest.Security["secretScan"] && manifest.Security["dependencyScan"] { + score += 20 + } else { + score += 12 + } + if manifest.Evidence["witnessCompatible"] { + score += 15 + } else { + score += 8 + } + if manifest.Repository["url"] != "" && manifest.Documentation["url"] != "" { + score += 15 + } else { + score += 10 + } + if hasCapability(manifest.Capabilities, "proofstream") { + score += 15 + } else { + score += 10 + } + score += 5 + } + tier := "hidden" + switch { + case score >= 95: + tier = "platinum" + case score >= 85: + tier = "gold" + case score >= 70: + tier = "silver" + case score >= 50: + tier = "community" + } + if score < 50 && len(findings) == 0 { + findings = append(findings, Finding{"score.hidden", "error", "Evidence Score below 50 cannot be publicly listed"}) + } + return ValidationResult{OK: len(findings) == 0 && score >= 50, EvidenceScore: score, Tier: tier, Findings: findings} +} + +func hasCapability(values []string, target string) bool { + for _, value := range values { + if value == target { + return true + } + } + return false +} diff --git a/connectorkit/manifest_test.go b/connectorkit/manifest_test.go new file mode 100644 index 0000000..fbd194e --- /dev/null +++ b/connectorkit/manifest_test.go @@ -0,0 +1,30 @@ +package connectorkit + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestValidateFirstPartyManifests(t *testing.T) { + for _, rel := range []string{ + "../../../connectors/github/attesto.connector.json", + "../../../connectors/gitlab/attesto.connector.json", + "../../../connectors/s3/attesto.connector.json", + } { + path := filepath.Clean(rel) + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read manifest %s: %v", rel, err) + } + var manifest Manifest + if err := json.Unmarshal(raw, &manifest); err != nil { + t.Fatalf("parse manifest %s: %v", rel, err) + } + result := ValidateManifest(manifest) + if !result.OK || result.EvidenceScore != 95 || result.Tier != "platinum" { + t.Fatalf("unexpected validation result for %s: %+v", rel, result) + } + } +}