Add connector v2 admin operations
This commit is contained in:
@@ -1002,14 +1002,23 @@ func (a *app) marketplaceInit(args []string) error {
|
|||||||
repositoryURL := fs.String("repository-url", "", "public release source URL")
|
repositoryURL := fs.String("repository-url", "", "public release source URL")
|
||||||
docsURL := fs.String("docs-url", "", "public documentation URL")
|
docsURL := fs.String("docs-url", "", "public documentation URL")
|
||||||
capabilitiesRaw := fs.String("capabilities", "", "comma-separated capabilities")
|
capabilitiesRaw := fs.String("capabilities", "", "comma-separated capabilities")
|
||||||
|
providerURL := fs.String("provider-url", "", "provider website URL")
|
||||||
|
authMode := fs.String("auth-mode", "", "connector auth mode")
|
||||||
|
authScopesRaw := fs.String("auth-scopes", "", "comma-separated provider scopes")
|
||||||
|
syncModesRaw := fs.String("sync-modes", "", "comma-separated sync modes")
|
||||||
|
eventTypesRaw := fs.String("event-types", "", "comma-separated source event types")
|
||||||
|
canaryRef := fs.String("canary-ref", "", "real canary/test-account evidence reference")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if *output == "" {
|
if *output == "" {
|
||||||
return errors.New("output is required")
|
return errors.New("output is required")
|
||||||
}
|
}
|
||||||
|
authScopes := splitCSV(*authScopesRaw)
|
||||||
|
syncModes := splitCSV(*syncModesRaw)
|
||||||
|
eventTypes := splitCSV(*eventTypesRaw)
|
||||||
manifest := connectorkit.Manifest{
|
manifest := connectorkit.Manifest{
|
||||||
SchemaVersion: "1.0",
|
SchemaVersion: "attesto.connector.v2",
|
||||||
Slug: *slug,
|
Slug: *slug,
|
||||||
Name: *name,
|
Name: *name,
|
||||||
Version: *version,
|
Version: *version,
|
||||||
@@ -1036,6 +1045,62 @@ func (a *app) marketplaceInit(args []string) error {
|
|||||||
"secretScan": true,
|
"secretScan": true,
|
||||||
},
|
},
|
||||||
SupportedLanguages: []string{"en", "nl", "de", "fr", "es", "pl", "it"},
|
SupportedLanguages: []string{"en", "nl", "de", "fr", "es", "pl", "it"},
|
||||||
|
Provider: map[string]any{
|
||||||
|
"id": *slug,
|
||||||
|
"name": *name,
|
||||||
|
"websiteUrl": *providerURL,
|
||||||
|
},
|
||||||
|
Auth: map[string]any{
|
||||||
|
"mode": *authMode,
|
||||||
|
"scopes": authScopes,
|
||||||
|
},
|
||||||
|
Sync: map[string]any{
|
||||||
|
"modes": syncModes,
|
||||||
|
"supportsReplay": true,
|
||||||
|
"rateLimitPolicy": "provider-documented",
|
||||||
|
},
|
||||||
|
EventTypes: eventTypes,
|
||||||
|
SourceTime: map[string]any{
|
||||||
|
"required": true,
|
||||||
|
"timezonePolicy": "source-timestamp-with-offset-required",
|
||||||
|
},
|
||||||
|
ConfigSchema: map[string]any{"type": "object"},
|
||||||
|
SecretSchema: map[string]any{"type": "object"},
|
||||||
|
Diagnostics: map[string]any{
|
||||||
|
"providerAuthStatus": true,
|
||||||
|
"replayConflictCheck": true,
|
||||||
|
"revocationCheck": true,
|
||||||
|
"syncLag": true,
|
||||||
|
"testConnection": true,
|
||||||
|
},
|
||||||
|
Runtime: map[string]any{
|
||||||
|
"officialConnectorKit": true,
|
||||||
|
"sdkSurfaces": []string{"python", "typescript", "go", "cli"},
|
||||||
|
"requiredMethods": []string{
|
||||||
|
"metadata",
|
||||||
|
"validateConfig",
|
||||||
|
"testConnection",
|
||||||
|
"sync",
|
||||||
|
"handleWebhook",
|
||||||
|
"emitProofstreamEvent",
|
||||||
|
"diagnostics",
|
||||||
|
"revoke",
|
||||||
|
},
|
||||||
|
"canary": map[string]any{
|
||||||
|
"status": "green",
|
||||||
|
"ref": *canaryRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InstallRequirements: map[string]any{
|
||||||
|
"tenantLoginRequired": true,
|
||||||
|
"entitlementRequired": true,
|
||||||
|
},
|
||||||
|
Changelog: []map[string]any{
|
||||||
|
{
|
||||||
|
"version": *version,
|
||||||
|
"changes": []string{"Initial production connector manifest."},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if len(manifest.Capabilities) == 0 {
|
if len(manifest.Capabilities) == 0 {
|
||||||
return errors.New("at least one capability is required")
|
return errors.New("at least one capability is required")
|
||||||
|
|||||||
@@ -201,6 +201,12 @@ func TestMarketplaceInitAndValidate(t *testing.T) {
|
|||||||
"--repository-url", "https://git.example.com/acme/risk-connector",
|
"--repository-url", "https://git.example.com/acme/risk-connector",
|
||||||
"--docs-url", "https://docs.example.com/acme/risk-connector",
|
"--docs-url", "https://docs.example.com/acme/risk-connector",
|
||||||
"--capabilities", "proofstream,offline-verification",
|
"--capabilities", "proofstream,offline-verification",
|
||||||
|
"--provider-url", "https://example.com/acme-risk",
|
||||||
|
"--auth-mode", "signed-webhook",
|
||||||
|
"--auth-scopes", "repository:read",
|
||||||
|
"--sync-modes", "webhook",
|
||||||
|
"--event-types", "risk.decision.created",
|
||||||
|
"--canary-ref", "attesto-owned-test-account-2026-06-09",
|
||||||
}, &stdout, &stderr, testEnv(t, nil))
|
}, &stdout, &stderr, testEnv(t, nil))
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
|
t.Fatalf("exit=%d stderr=%s", code, stderr.String())
|
||||||
@@ -311,7 +317,7 @@ func loadVector(t *testing.T) map[string]any {
|
|||||||
func writeMarketplaceManifest(t *testing.T, path string) {
|
func writeMarketplaceManifest(t *testing.T, path string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
manifest := map[string]any{
|
manifest := map[string]any{
|
||||||
"schemaVersion": "1.0",
|
"schemaVersion": "attesto.connector.v2",
|
||||||
"slug": "acme-risk-connector",
|
"slug": "acme-risk-connector",
|
||||||
"name": "ACME Risk Connector",
|
"name": "ACME Risk Connector",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -340,6 +346,62 @@ func writeMarketplaceManifest(t *testing.T, path string) {
|
|||||||
"secretScan": true,
|
"secretScan": true,
|
||||||
},
|
},
|
||||||
"supportedLanguages": []string{"en", "nl", "de", "fr", "es", "pl", "it"},
|
"supportedLanguages": []string{"en", "nl", "de", "fr", "es", "pl", "it"},
|
||||||
|
"provider": map[string]any{
|
||||||
|
"id": "acme-risk-connector",
|
||||||
|
"name": "ACME Risk Connector",
|
||||||
|
"websiteUrl": "https://example.com/acme-risk",
|
||||||
|
},
|
||||||
|
"auth": map[string]any{
|
||||||
|
"mode": "signed-webhook",
|
||||||
|
"scopes": []string{"repository:read"},
|
||||||
|
},
|
||||||
|
"sync": map[string]any{
|
||||||
|
"modes": []string{"webhook"},
|
||||||
|
"supportsReplay": true,
|
||||||
|
"rateLimitPolicy": "provider-documented",
|
||||||
|
},
|
||||||
|
"eventTypes": []string{"risk.decision.created"},
|
||||||
|
"sourceTime": map[string]any{
|
||||||
|
"required": true,
|
||||||
|
"timezonePolicy": "source-timestamp-with-offset-required",
|
||||||
|
},
|
||||||
|
"configSchema": map[string]any{"type": "object"},
|
||||||
|
"secretSchema": map[string]any{"type": "object"},
|
||||||
|
"diagnostics": map[string]any{
|
||||||
|
"providerAuthStatus": true,
|
||||||
|
"replayConflictCheck": true,
|
||||||
|
"revocationCheck": true,
|
||||||
|
"syncLag": true,
|
||||||
|
"testConnection": true,
|
||||||
|
},
|
||||||
|
"runtime": map[string]any{
|
||||||
|
"officialConnectorKit": true,
|
||||||
|
"sdkSurfaces": []string{"python", "typescript", "go", "cli"},
|
||||||
|
"requiredMethods": []string{
|
||||||
|
"metadata",
|
||||||
|
"validateConfig",
|
||||||
|
"testConnection",
|
||||||
|
"sync",
|
||||||
|
"handleWebhook",
|
||||||
|
"emitProofstreamEvent",
|
||||||
|
"diagnostics",
|
||||||
|
"revoke",
|
||||||
|
},
|
||||||
|
"canary": map[string]any{
|
||||||
|
"status": "green",
|
||||||
|
"ref": "attesto-owned-test-account-2026-06-09",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"installRequirements": map[string]any{
|
||||||
|
"tenantLoginRequired": true,
|
||||||
|
"entitlementRequired": true,
|
||||||
|
},
|
||||||
|
"changelog": []map[string]any{
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"changes": []string{"Initial production connector manifest."},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
raw, err := json.MarshalIndent(manifest, "", " ")
|
raw, err := json.MarshalIndent(manifest, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,21 +3,32 @@ package connectorkit
|
|||||||
import "regexp"
|
import "regexp"
|
||||||
|
|
||||||
type Manifest struct {
|
type Manifest struct {
|
||||||
SchemaVersion string `json:"schemaVersion"`
|
SchemaVersion string `json:"schemaVersion"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
AssetType string `json:"assetType"`
|
AssetType string `json:"assetType"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
Summary string `json:"summary,omitempty"`
|
Summary string `json:"summary,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Publisher map[string]string `json:"publisher"`
|
Publisher map[string]string `json:"publisher"`
|
||||||
Repository map[string]string `json:"repository"`
|
Repository map[string]string `json:"repository"`
|
||||||
Documentation map[string]string `json:"documentation"`
|
Documentation map[string]string `json:"documentation"`
|
||||||
Capabilities []string `json:"capabilities"`
|
Capabilities []string `json:"capabilities"`
|
||||||
Evidence map[string]bool `json:"evidence"`
|
Evidence map[string]bool `json:"evidence"`
|
||||||
Security map[string]bool `json:"security"`
|
Security map[string]bool `json:"security"`
|
||||||
SupportedLanguages []string `json:"supportedLanguages,omitempty"`
|
SupportedLanguages []string `json:"supportedLanguages,omitempty"`
|
||||||
|
Provider map[string]any `json:"provider,omitempty"`
|
||||||
|
Auth map[string]any `json:"auth,omitempty"`
|
||||||
|
Sync map[string]any `json:"sync,omitempty"`
|
||||||
|
EventTypes []string `json:"eventTypes,omitempty"`
|
||||||
|
SourceTime map[string]any `json:"sourceTime,omitempty"`
|
||||||
|
ConfigSchema map[string]any `json:"configSchema,omitempty"`
|
||||||
|
SecretSchema map[string]any `json:"secretSchema,omitempty"`
|
||||||
|
Diagnostics map[string]any `json:"diagnostics,omitempty"`
|
||||||
|
Runtime map[string]any `json:"runtime,omitempty"`
|
||||||
|
InstallRequirements map[string]any `json:"installRequirements,omitempty"`
|
||||||
|
Changelog []map[string]any `json:"changelog,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Finding struct {
|
type Finding struct {
|
||||||
@@ -37,13 +48,27 @@ 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 versionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:[-+][a-zA-Z0-9.-]+)?$`)
|
||||||
var categories = map[string]bool{
|
var categories = map[string]bool{
|
||||||
"ai-governance": true,
|
"ai-governance": true,
|
||||||
"compliance": true,
|
"compliance": true,
|
||||||
"crm": true,
|
"crm": true,
|
||||||
"devops": true,
|
"devops": true,
|
||||||
"erp": true,
|
"erp": true,
|
||||||
"iam": true,
|
"iam": true,
|
||||||
"security": true,
|
"security": true,
|
||||||
"storage": true,
|
"storage": true,
|
||||||
|
}
|
||||||
|
var schemaVersions = map[string]bool{"1.0": true, "attesto.connector.v2": true}
|
||||||
|
var supportedAuthModes = map[string]bool{
|
||||||
|
"api-key": true, "basic-auth": true, "oauth2": true, "signed-webhook": true,
|
||||||
|
"storage-credentials": true, "token": true,
|
||||||
|
}
|
||||||
|
var supportedSyncModes = map[string]bool{
|
||||||
|
"manual": true, "object-commit": true, "polling": true, "webhook": true,
|
||||||
|
}
|
||||||
|
var requiredDiagnostics = []string{
|
||||||
|
"providerAuthStatus", "replayConflictCheck", "revocationCheck", "syncLag", "testConnection",
|
||||||
|
}
|
||||||
|
var requiredRuntimeMethods = []string{
|
||||||
|
"diagnostics", "emitProofstreamEvent", "handleWebhook", "metadata", "revoke", "sync", "testConnection", "validateConfig",
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateManifest(manifest Manifest) ValidationResult {
|
func ValidateManifest(manifest Manifest) ValidationResult {
|
||||||
@@ -54,6 +79,9 @@ func ValidateManifest(manifest Manifest) ValidationResult {
|
|||||||
if !versionPattern.MatchString(manifest.Version) {
|
if !versionPattern.MatchString(manifest.Version) {
|
||||||
findings = append(findings, Finding{"manifest.invalid_version", "error", "version must be semantic versioning"})
|
findings = append(findings, Finding{"manifest.invalid_version", "error", "version must be semantic versioning"})
|
||||||
}
|
}
|
||||||
|
if !schemaVersions[manifest.SchemaVersion] {
|
||||||
|
findings = append(findings, Finding{"manifest.invalid_schema_version", "error", "schemaVersion must be 1.0 or attesto.connector.v2"})
|
||||||
|
}
|
||||||
if !categories[manifest.Category] {
|
if !categories[manifest.Category] {
|
||||||
findings = append(findings, Finding{"manifest.invalid_category", "error", "unsupported category"})
|
findings = append(findings, Finding{"manifest.invalid_category", "error", "unsupported category"})
|
||||||
}
|
}
|
||||||
@@ -63,6 +91,9 @@ func ValidateManifest(manifest Manifest) ValidationResult {
|
|||||||
if manifest.SchemaVersion == "" || manifest.Name == "" || manifest.AssetType == "" {
|
if manifest.SchemaVersion == "" || manifest.Name == "" || manifest.AssetType == "" {
|
||||||
findings = append(findings, Finding{"manifest.missing_field", "error", "schemaVersion, name, and assetType are required"})
|
findings = append(findings, Finding{"manifest.missing_field", "error", "schemaVersion, name, and assetType are required"})
|
||||||
}
|
}
|
||||||
|
if manifest.AssetType == "connector" && manifest.SchemaVersion == "attesto.connector.v2" {
|
||||||
|
findings = appendV2Findings(manifest, findings)
|
||||||
|
}
|
||||||
score := 0
|
score := 0
|
||||||
if len(findings) == 0 {
|
if len(findings) == 0 {
|
||||||
score = 0
|
score = 0
|
||||||
@@ -110,6 +141,125 @@ func ValidateManifest(manifest Manifest) ValidationResult {
|
|||||||
return ValidationResult{OK: len(findings) == 0 && score >= 50, EvidenceScore: score, Tier: tier, Findings: findings}
|
return ValidationResult{OK: len(findings) == 0 && score >= 50, EvidenceScore: score, Tier: tier, Findings: findings}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func appendV2Findings(manifest Manifest, findings []Finding) []Finding {
|
||||||
|
if !hasRequiredKeys(manifest.Provider, []string{"id", "name", "websiteUrl"}) {
|
||||||
|
findings = append(findings, Finding{"provider.invalid", "error", "provider.id, provider.name and provider.websiteUrl are required"})
|
||||||
|
}
|
||||||
|
if !hasRequiredKeys(manifest.Auth, []string{"mode", "scopes"}) {
|
||||||
|
findings = append(findings, Finding{"auth.invalid", "error", "auth.mode and auth.scopes are required"})
|
||||||
|
} else if !supportedAuthModes[toString(manifest.Auth["mode"])] {
|
||||||
|
findings = append(findings, Finding{"auth.invalid_mode", "error", "unsupported auth.mode"})
|
||||||
|
} else if len(stringSet(manifest.Auth["scopes"])) == 0 {
|
||||||
|
findings = append(findings, Finding{"auth.invalid_scopes", "error", "auth.scopes must be a non-empty list"})
|
||||||
|
}
|
||||||
|
if !hasRequiredKeys(manifest.Sync, []string{"modes", "supportsReplay", "rateLimitPolicy"}) {
|
||||||
|
findings = append(findings, Finding{"sync.invalid", "error", "sync.modes, sync.supportsReplay and sync.rateLimitPolicy are required"})
|
||||||
|
} else {
|
||||||
|
modes := stringSet(manifest.Sync["modes"])
|
||||||
|
if len(modes) == 0 || !subset(modes, supportedSyncModes) {
|
||||||
|
findings = append(findings, Finding{"sync.invalid_modes", "error", "unsupported sync.modes"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(manifest.EventTypes) == 0 {
|
||||||
|
findings = append(findings, Finding{"event_types.invalid", "error", "eventTypes must be non-empty strings"})
|
||||||
|
}
|
||||||
|
if !hasRequiredKeys(manifest.SourceTime, []string{"required", "timezonePolicy"}) {
|
||||||
|
findings = append(findings, Finding{"source_time.invalid", "error", "sourceTime.required and sourceTime.timezonePolicy are required"})
|
||||||
|
} else if manifest.SourceTime["required"] != true {
|
||||||
|
findings = append(findings, Finding{"source_time.not_required", "error", "sourceTime.required must be true for production connectors"})
|
||||||
|
}
|
||||||
|
if manifest.ConfigSchema["type"] != "object" {
|
||||||
|
findings = append(findings, Finding{"configSchema.invalid", "error", "configSchema.type must be object"})
|
||||||
|
}
|
||||||
|
if manifest.SecretSchema["type"] != "object" {
|
||||||
|
findings = append(findings, Finding{"secretSchema.invalid", "error", "secretSchema.type must be object"})
|
||||||
|
}
|
||||||
|
if !hasRequiredKeys(manifest.Diagnostics, requiredDiagnostics) {
|
||||||
|
findings = append(findings, Finding{"diagnostics.incomplete", "error", "diagnostics must cover auth, connection, lag, replay and revoke state"})
|
||||||
|
}
|
||||||
|
if !hasRequiredKeys(manifest.Runtime, []string{"officialConnectorKit", "sdkSurfaces", "requiredMethods", "canary"}) {
|
||||||
|
findings = append(findings, Finding{"runtime.incomplete", "error", "runtime must declare connector-kit, SDK surfaces, methods and canary"})
|
||||||
|
} else {
|
||||||
|
if manifest.Runtime["officialConnectorKit"] != true {
|
||||||
|
findings = append(findings, Finding{"runtime.unofficial", "error", "runtime.officialConnectorKit must be true"})
|
||||||
|
}
|
||||||
|
surfaces := stringSet(manifest.Runtime["sdkSurfaces"])
|
||||||
|
for _, surface := range []string{"python", "typescript", "go", "cli"} {
|
||||||
|
if !surfaces[surface] {
|
||||||
|
findings = append(findings, Finding{"runtime.sdk_surfaces", "error", "runtime.sdkSurfaces must include python, typescript, go and cli"})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
methods := stringSet(manifest.Runtime["requiredMethods"])
|
||||||
|
for _, method := range requiredRuntimeMethods {
|
||||||
|
if !methods[method] {
|
||||||
|
findings = append(findings, Finding{"runtime.required_methods", "error", "runtime.requiredMethods is missing connector runtime methods"})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canary, ok := manifest.Runtime["canary"].(map[string]any)
|
||||||
|
if !ok || canary["status"] != "green" {
|
||||||
|
findings = append(findings, Finding{"runtime.canary", "error", "runtime.canary.status must be green before publication"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRequiredKeys(manifest.InstallRequirements, []string{"tenantLoginRequired", "entitlementRequired"}) {
|
||||||
|
findings = append(findings, Finding{"install_requirements.invalid", "error", "installRequirements must declare tenant login and entitlement requirements"})
|
||||||
|
} else if manifest.InstallRequirements["tenantLoginRequired"] != true || manifest.InstallRequirements["entitlementRequired"] != true {
|
||||||
|
findings = append(findings, Finding{"install_requirements.unsafe", "error", "install/download/configure must require tenant login and entitlement"})
|
||||||
|
}
|
||||||
|
if len(manifest.Changelog) == 0 {
|
||||||
|
findings = append(findings, Finding{"changelog.invalid", "error", "changelog must include versioned entries"})
|
||||||
|
}
|
||||||
|
return findings
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasRequiredKeys(value map[string]any, keys []string) bool {
|
||||||
|
if value == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
if _, ok := value[key]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func toString(value any) string {
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringSet(value any) map[string]bool {
|
||||||
|
out := map[string]bool{}
|
||||||
|
switch values := value.(type) {
|
||||||
|
case []string:
|
||||||
|
for _, item := range values {
|
||||||
|
if item != "" {
|
||||||
|
out[item] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []any:
|
||||||
|
for _, item := range values {
|
||||||
|
if text, ok := item.(string); ok && text != "" {
|
||||||
|
out[text] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func subset(values map[string]bool, allowed map[string]bool) bool {
|
||||||
|
for value := range values {
|
||||||
|
if !allowed[value] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func hasCapability(values []string, target string) bool {
|
func hasCapability(values []string, target string) bool {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if value == target {
|
if value == target {
|
||||||
|
|||||||
@@ -28,3 +28,33 @@ func TestValidateFirstPartyManifests(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateV2ManifestRequiresRuntimeMetadata(t *testing.T) {
|
||||||
|
raw, err := os.ReadFile(filepath.Clean("../../../connectors/github/attesto.connector.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var manifest Manifest
|
||||||
|
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if manifest.SchemaVersion != "attesto.connector.v2" {
|
||||||
|
t.Fatalf("unexpected schema version: %s", manifest.SchemaVersion)
|
||||||
|
}
|
||||||
|
manifest.Runtime = nil
|
||||||
|
|
||||||
|
result := ValidateManifest(manifest)
|
||||||
|
|
||||||
|
if result.OK {
|
||||||
|
t.Fatalf("expected invalid manifest without runtime metadata")
|
||||||
|
}
|
||||||
|
var found bool
|
||||||
|
for _, finding := range result.Findings {
|
||||||
|
if finding.Code == "runtime.incomplete" {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("missing runtime finding: %+v", result.Findings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user