fix: harden connector manifest validation

This commit is contained in:
Codex
2026-06-16 23:40:33 +02:00
parent 4a4b86ae10
commit 8d6d9bf9c2
5 changed files with 289 additions and 28 deletions
+43 -17
View File
@@ -22,7 +22,13 @@ import (
var connectorSlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,95}$`) var connectorSlugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,95}$`)
func connectorManifestTemplate(slug, name, category string) connectorkit.Manifest { func connectorManifestTemplate(
slug, name, category, publisherSlug, publisherName, repositoryURL, docsURL, providerURL, canaryRef string,
) connectorkit.Manifest {
canaryStatus := "pending"
if strings.TrimSpace(canaryRef) != "" {
canaryStatus = "green"
}
return connectorkit.Manifest{ return connectorkit.Manifest{
SchemaVersion: "attesto.connector.v2", SchemaVersion: "attesto.connector.v2",
Slug: slug, Slug: slug,
@@ -33,9 +39,9 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
Summary: fmt.Sprintf("Verify %s evidence into Attesto Proofstream.", name), Summary: fmt.Sprintf("Verify %s evidence into Attesto Proofstream.", name),
Description: fmt.Sprintf( Description: fmt.Sprintf(
"Produces verifiable evidence for %s events through Attesto Proofstream.", name), "Produces verifiable evidence for %s events through Attesto Proofstream.", name),
Publisher: map[string]string{"name": "CHANGE ME", "slug": "change-me"}, Publisher: map[string]string{"name": publisherName, "slug": publisherSlug},
Repository: map[string]string{"url": "https://example.com/CHANGE-ME/" + slug}, Repository: map[string]string{"url": repositoryURL},
Documentation: map[string]string{"url": "https://docs.attesto.eu/manuals/connectors.html"}, Documentation: map[string]string{"url": docsURL},
Capabilities: []string{ Capabilities: []string{
"proofstream", "signed-webhook", "offline-verification", "proofstream", "signed-webhook", "offline-verification",
}, },
@@ -53,7 +59,7 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
Provider: map[string]any{ Provider: map[string]any{
"id": slug, "id": slug,
"name": name, "name": name,
"websiteUrl": "https://example.com", "websiteUrl": providerURL,
}, },
Auth: map[string]any{ Auth: map[string]any{
"mode": "signed-webhook", "mode": "signed-webhook",
@@ -97,12 +103,9 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
"metadata", "validateConfig", "testConnection", "sync", "metadata", "validateConfig", "testConnection", "sync",
"handleWebhook", "emitProofstreamEvent", "diagnostics", "revoke", "handleWebhook", "emitProofstreamEvent", "diagnostics", "revoke",
}, },
// A scaffold cannot honestly claim a green assurance canary; this
// stays "pending" (the one expected validation finding) until the
// connector has real canary evidence.
"canary": map[string]any{ "canary": map[string]any{
"status": "pending", "status": canaryStatus,
"ref": "CHANGE ME: assurance canary evidence ref", "ref": canaryRef,
}, },
}, },
InstallRequirements: map[string]any{ InstallRequirements: map[string]any{
@@ -115,7 +118,7 @@ func connectorManifestTemplate(slug, name, category string) connectorkit.Manifes
} }
} }
const webhookHandlerTemplate = `"""Signed-webhook handler starter for the %s connector. const webhookHandlerTemplate = `"""Signed-webhook verification helper for the %s connector.
Verification uses the Attesto SDK's P1.4 helper — the same scheme the Verification uses the Attesto SDK's P1.4 helper — the same scheme the
platform signs with: HMAC-SHA256 over "{timestamp}.{body}" with a 300s platform signs with: HMAC-SHA256 over "{timestamp}.{body}" with a 300s
@@ -134,8 +137,7 @@ def handle(headers: dict[str, str], body: bytes) -> dict:
): ):
raise PermissionError("invalid webhook signature or stale timestamp") raise PermissionError("invalid webhook signature or stale timestamp")
# The payload is now authentic: turn it into a proofstream event here. return {"ok": True, "authenticatedPayloadBytes": len(body)}
return {"ok": True}
` `
const connectorReadmeTemplate = `# %s const connectorReadmeTemplate = `# %s
@@ -143,7 +145,7 @@ const connectorReadmeTemplate = `# %s
Scaffolded by ` + "`attesto connector init`" + `. Scaffolded by ` + "`attesto connector init`" + `.
1. Edit ` + "`attesto.connector.json`" + ` (publisher, repository, provider, 1. Edit ` + "`attesto.connector.json`" + ` (publisher, repository, provider,
event types — search for CHANGE ME). event types and canary evidence when applicable).
2. Implement the runtime methods (see ` + "`webhook_handler.py`" + ` for the 2. Implement the runtime methods (see ` + "`webhook_handler.py`" + ` for the
signed-webhook entry point; verification is already wired). signed-webhook entry point; verification is already wired).
3. Re-run the pre-submission check at any time: 3. Re-run the pre-submission check at any time:
@@ -159,6 +161,12 @@ func (a *app) connectorInit(args []string) error {
name := fs.String("name", "", "human-readable connector name (default: derived from slug)") name := fs.String("name", "", "human-readable connector name (default: derived from slug)")
category := fs.String("category", "devops", "marketplace category") category := fs.String("category", "devops", "marketplace category")
dir := fs.String("dir", "", "output directory (default: ./<slug>)") dir := fs.String("dir", "", "output directory (default: ./<slug>)")
publisherSlug := fs.String("publisher-slug", "", "publisher slug")
publisherName := fs.String("publisher-name", "", "publisher display name")
repositoryURL := fs.String("repository-url", "", "HTTPS repository URL for this connector")
docsURL := fs.String("docs-url", "https://docs.attesto.eu/manuals/connectors.html", "HTTPS documentation URL")
providerURL := fs.String("provider-url", "", "HTTPS provider/product URL")
canaryRef := fs.String("canary-ref", "", "optional real canary/release evidence reference; required before publication")
validateOnly := fs.String("validate-only", "", "validate an existing <dir>/attesto.connector.json and exit") validateOnly := fs.String("validate-only", "", "validate an existing <dir>/attesto.connector.json and exit")
// Accept the slug positionally before flags: `connector init my-slug --category crm`. // Accept the slug positionally before flags: `connector init my-slug --category crm`.
slug := "" slug := ""
@@ -198,12 +206,31 @@ func (a *app) connectorInit(args []string) error {
if !connectorSlugPattern.MatchString(slug) { if !connectorSlugPattern.MatchString(slug) {
return fmt.Errorf("slug %q must match %s", slug, connectorSlugPattern) return fmt.Errorf("slug %q must match %s", slug, connectorSlugPattern)
} }
if strings.TrimSpace(*publisherSlug) == "" || strings.TrimSpace(*publisherName) == "" {
return errors.New("--publisher-slug and --publisher-name are required; connector init never writes placeholder publisher metadata")
}
if strings.TrimSpace(*repositoryURL) == "" || strings.TrimSpace(*providerURL) == "" {
return errors.New("--repository-url and --provider-url are required; connector init never writes placeholder URLs")
}
if strings.TrimSpace(*docsURL) == "" {
return errors.New("--docs-url is required")
}
connectorName := *name connectorName := *name
if connectorName == "" { if connectorName == "" {
connectorName = strings.Title(strings.ReplaceAll(slug, "-", " ")) //nolint:staticcheck connectorName = strings.Title(strings.ReplaceAll(slug, "-", " ")) //nolint:staticcheck
} }
manifest := connectorManifestTemplate(slug, connectorName, *category) manifest := connectorManifestTemplate(
slug,
connectorName,
*category,
*publisherSlug,
*publisherName,
*repositoryURL,
*docsURL,
*providerURL,
*canaryRef,
)
result := connectorkit.ValidateManifest(manifest) result := connectorkit.ValidateManifest(manifest)
// The only acceptable finding on a fresh scaffold is the pending canary — // The only acceptable finding on a fresh scaffold is the pending canary —
// everything else must already satisfy the marketplace validator. // everything else must already satisfy the marketplace validator.
@@ -242,7 +269,6 @@ func (a *app) connectorInit(args []string) error {
"created": outDir, "created": outDir,
"files": []string{"attesto.connector.json", "webhook_handler.py", "README.md"}, "files": []string{"attesto.connector.json", "webhook_handler.py", "README.md"},
"validation": result, "validation": result,
"nextSteps": "edit CHANGE ME fields, implement runtime methods, earn a green " + "nextSteps": "implement runtime methods, attach real canary evidence, re-run with --validate-only until OK",
"assurance canary, re-run with --validate-only until OK",
}) })
} }
+34 -2
View File
@@ -18,13 +18,26 @@ import (
func TestConnectorInitScaffoldsValidManifest(t *testing.T) { func TestConnectorInitScaffoldsValidManifest(t *testing.T) {
dir := filepath.Join(t.TempDir(), "my-crm") dir := filepath.Join(t.TempDir(), "my-crm")
a := &app{out: &bytes.Buffer{}, err: &bytes.Buffer{}} a := &app{out: &bytes.Buffer{}, err: &bytes.Buffer{}}
if err := a.connectorInit([]string{"my-crm-evidence", "--category", "crm", "--dir", dir}); err != nil { if err := a.connectorInit([]string{
"my-crm-evidence",
"--category", "crm",
"--dir", dir,
"--publisher-slug", "attesto",
"--publisher-name", "Attesto",
"--repository-url", "https://git.rotz.ai/rotzmediagroup/my-crm-evidence",
"--provider-url", "https://attesto.eu",
}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
raw, err := os.ReadFile(filepath.Join(dir, "attesto.connector.json")) raw, err := os.ReadFile(filepath.Join(dir, "attesto.connector.json"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if strings.Contains(strings.ToLower(string(raw)), "change me") ||
strings.Contains(strings.ToLower(string(raw)), "change-me") ||
strings.Contains(strings.ToLower(string(raw)), "example.com") {
t.Fatalf("scaffold contains placeholder metadata: %s", string(raw))
}
var manifest connectorkit.Manifest var manifest connectorkit.Manifest
if err := json.Unmarshal(raw, &manifest); err != nil { if err := json.Unmarshal(raw, &manifest); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -43,7 +56,26 @@ func TestConnectorInitScaffoldsValidManifest(t *testing.T) {
t.Fatal("generated handler does not use the P1.4 helper") t.Fatal("generated handler does not use the P1.4 helper")
} }
// re-running must refuse to overwrite // re-running must refuse to overwrite
if err := a.connectorInit([]string{"my-crm-evidence", "--dir", dir}); err == nil { if err := a.connectorInit([]string{
"my-crm-evidence",
"--dir", dir,
"--publisher-slug", "attesto",
"--publisher-name", "Attesto",
"--repository-url", "https://git.rotz.ai/rotzmediagroup/my-crm-evidence",
"--provider-url", "https://attesto.eu",
}); err == nil {
t.Fatal("expected overwrite refusal") t.Fatal("expected overwrite refusal")
} }
} }
func TestConnectorInitRejectsMissingRealMetadata(t *testing.T) {
dir := filepath.Join(t.TempDir(), "my-crm")
a := &app{out: &bytes.Buffer{}, err: &bytes.Buffer{}}
err := a.connectorInit([]string{"my-crm-evidence", "--category", "crm", "--dir", dir})
if err == nil {
t.Fatal("expected connector init to require real metadata")
}
if _, statErr := os.Stat(filepath.Join(dir, "attesto.connector.json")); !os.IsNotExist(statErr) {
t.Fatalf("connector init wrote files after missing metadata error: %v", statErr)
}
}
+85 -8
View File
@@ -198,10 +198,10 @@ func TestMarketplaceInitAndValidate(t *testing.T) {
"--description", "Produces verifiable Proofstream events for ACME risk decisions.", "--description", "Produces verifiable Proofstream events for ACME risk decisions.",
"--publisher-slug", "acme", "--publisher-slug", "acme",
"--publisher-name", "ACME", "--publisher-name", "ACME",
"--repository-url", "https://git.example.com/acme/risk-connector", "--repository-url", "https://git.rotz.ai/rotzmediagroup/acme-risk-connector",
"--docs-url", "https://docs.example.com/acme/risk-connector", "--docs-url", "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
"--capabilities", "proofstream,offline-verification", "--capabilities", "proofstream,offline-verification",
"--provider-url", "https://example.com/acme-risk", "--provider-url", "https://attesto.eu/connectors/acme-risk",
"--auth-mode", "signed-webhook", "--auth-mode", "signed-webhook",
"--auth-scopes", "repository:read", "--auth-scopes", "repository:read",
"--sync-modes", "webhook", "--sync-modes", "webhook",
@@ -226,6 +226,83 @@ func TestMarketplaceInitAndValidate(t *testing.T) {
} }
} }
func TestMarketplaceInitRejectsMissingCanaryEvidenceRef(t *testing.T) {
dir := t.TempDir()
manifestFile := filepath.Join(dir, "attesto.connector.json")
var stdout, stderr bytes.Buffer
code := run([]string{
"--json",
"marketplace",
"init",
"--output", manifestFile,
"--slug", "acme-risk-connector",
"--name", "ACME Risk Connector",
"--version", "1.0.0",
"--category", "ai-governance",
"--summary", "Produces Attesto evidence for ACME risk decisions.",
"--description", "Produces verifiable Proofstream events for ACME risk decisions.",
"--publisher-slug", "acme",
"--publisher-name", "ACME",
"--repository-url", "https://git.rotz.ai/rotzmediagroup/acme-risk-connector",
"--docs-url", "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
"--capabilities", "proofstream,offline-verification",
"--provider-url", "https://attesto.eu/connectors/acme-risk",
"--auth-mode", "signed-webhook",
"--auth-scopes", "repository:read",
"--sync-modes", "webhook",
"--event-types", "risk.decision.created",
}, &stdout, &stderr, testEnv(t, nil))
if code != 0 {
t.Fatalf("init exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"ok": false`) || !strings.Contains(stdout.String(), "runtime.canary_ref") {
t.Fatalf("expected missing canary evidence finding: %s", stdout.String())
}
if _, err := os.Stat(manifestFile); !os.IsNotExist(err) {
t.Fatalf("marketplace init wrote a manifest without canary evidence: %v", err)
}
}
func TestMarketplaceInitRejectsPlaceholderMetadata(t *testing.T) {
dir := t.TempDir()
manifestFile := filepath.Join(dir, "attesto.connector.json")
var stdout, stderr bytes.Buffer
code := run([]string{
"--json",
"marketplace",
"init",
"--output", manifestFile,
"--slug", "acme-risk-connector",
"--name", "ACME Risk Connector",
"--version", "1.0.0",
"--category", "ai-governance",
"--summary", "Produces Attesto evidence for ACME risk decisions.",
"--description", "Produces verifiable Proofstream events for ACME risk decisions.",
"--publisher-slug", "acme",
"--publisher-name", "ACME",
"--repository-url", "https://example.com/acme/risk-connector",
"--docs-url", "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
"--capabilities", "proofstream,offline-verification",
"--provider-url", "https://attesto.eu/connectors/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))
if code != 0 {
t.Fatalf("init exit=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), `"ok": false`) || !strings.Contains(stdout.String(), "manifest.placeholder") {
t.Fatalf("expected placeholder metadata finding: %s", stdout.String())
}
if _, err := os.Stat(manifestFile); !os.IsNotExist(err) {
t.Fatalf("marketplace init wrote a manifest with placeholder metadata: %v", err)
}
}
func TestMarketplaceSubmitAndPublishCallRealAPIs(t *testing.T) { func TestMarketplaceSubmitAndPublishCallRealAPIs(t *testing.T) {
dir := t.TempDir() dir := t.TempDir()
manifestFile := filepath.Join(dir, "attesto.connector.json") manifestFile := filepath.Join(dir, "attesto.connector.json")
@@ -245,7 +322,7 @@ func TestMarketplaceSubmitAndPublishCallRealAPIs(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("submit json: %v", err) t.Fatalf("submit json: %v", err)
} }
if body["sourceRef"] != "https://git.example.com/acme/risk-connector/releases/v1.0.0" { if body["sourceRef"] != "https://git.rotz.ai/rotzmediagroup/acme-risk-connector/releases/v1.0.0" {
t.Fatalf("unexpected sourceRef: %#v", body["sourceRef"]) t.Fatalf("unexpected sourceRef: %#v", body["sourceRef"])
} }
_, _ = w.Write([]byte(`{"asset":{"slug":"acme-risk-connector","name":"ACME Risk Connector"},"validation":{"ok":true},"evidence":{"action":"asset_validation_finished","receiptHash":"rh","payloadHash":"ph"}}`)) _, _ = w.Write([]byte(`{"asset":{"slug":"acme-risk-connector","name":"ACME Risk Connector"},"validation":{"ok":true},"evidence":{"action":"asset_validation_finished","receiptHash":"rh","payloadHash":"ph"}}`))
@@ -271,7 +348,7 @@ func TestMarketplaceSubmitAndPublishCallRealAPIs(t *testing.T) {
"--json", "--base-url", server.URL, "--token-env", "ATT_TOKEN", "--json", "--base-url", server.URL, "--token-env", "ATT_TOKEN",
"marketplace", "submit", "marketplace", "submit",
"--manifest-file", manifestFile, "--manifest-file", manifestFile,
"--source-ref", "https://git.example.com/acme/risk-connector/releases/v1.0.0", "--source-ref", "https://git.rotz.ai/rotzmediagroup/acme-risk-connector/releases/v1.0.0",
"--visibility", "public", "--visibility", "public",
"--pricing-model", "free", "--pricing-model", "free",
}, &stdout, &stderr, env) }, &stdout, &stderr, env)
@@ -330,10 +407,10 @@ func writeMarketplaceManifest(t *testing.T, path string) {
"name": "ACME", "name": "ACME",
}, },
"repository": map[string]any{ "repository": map[string]any{
"url": "https://git.example.com/acme/risk-connector", "url": "https://git.rotz.ai/rotzmediagroup/acme-risk-connector",
}, },
"documentation": map[string]any{ "documentation": map[string]any{
"url": "https://docs.example.com/acme/risk-connector", "url": "https://docs.attesto.eu/manuals/connectors.html#acme-risk-connector",
}, },
"capabilities": []string{"proofstream", "offline-verification"}, "capabilities": []string{"proofstream", "offline-verification"},
"evidence": map[string]bool{ "evidence": map[string]bool{
@@ -349,7 +426,7 @@ func writeMarketplaceManifest(t *testing.T, path string) {
"provider": map[string]any{ "provider": map[string]any{
"id": "acme-risk-connector", "id": "acme-risk-connector",
"name": "ACME Risk Connector", "name": "ACME Risk Connector",
"websiteUrl": "https://example.com/acme-risk", "websiteUrl": "https://attesto.eu/connectors/acme-risk",
}, },
"auth": map[string]any{ "auth": map[string]any{
"mode": "signed-webhook", "mode": "signed-webhook",
+73 -1
View File
@@ -1,6 +1,9 @@
package connectorkit package connectorkit
import "regexp" import (
"regexp"
"strings"
)
type Manifest struct { type Manifest struct {
SchemaVersion string `json:"schemaVersion"` SchemaVersion string `json:"schemaVersion"`
@@ -93,6 +96,7 @@ func ValidateManifest(manifest Manifest) ValidationResult {
} }
if manifest.AssetType == "connector" && manifest.SchemaVersion == "attesto.connector.v2" { if manifest.AssetType == "connector" && manifest.SchemaVersion == "attesto.connector.v2" {
findings = appendV2Findings(manifest, findings) findings = appendV2Findings(manifest, findings)
findings = appendPlaceholderFindings(manifest, findings)
} }
score := 0 score := 0
if len(findings) == 0 { if len(findings) == 0 {
@@ -142,6 +146,15 @@ func ValidateManifest(manifest Manifest) ValidationResult {
} }
func appendV2Findings(manifest Manifest, findings []Finding) []Finding { func appendV2Findings(manifest Manifest, findings []Finding) []Finding {
if strings.TrimSpace(manifest.Publisher["slug"]) == "" || strings.TrimSpace(manifest.Publisher["name"]) == "" {
findings = append(findings, Finding{"publisher.invalid", "error", "publisher.slug and publisher.name are required"})
}
if strings.TrimSpace(manifest.Repository["url"]) == "" {
findings = append(findings, Finding{"repository.invalid", "error", "repository.url is required"})
}
if strings.TrimSpace(manifest.Documentation["url"]) == "" {
findings = append(findings, Finding{"documentation.invalid", "error", "documentation.url is required"})
}
if !hasRequiredKeys(manifest.Provider, []string{"id", "name", "websiteUrl"}) { if !hasRequiredKeys(manifest.Provider, []string{"id", "name", "websiteUrl"}) {
findings = append(findings, Finding{"provider.invalid", "error", "provider.id, provider.name and provider.websiteUrl are required"}) findings = append(findings, Finding{"provider.invalid", "error", "provider.id, provider.name and provider.websiteUrl are required"})
} }
@@ -200,6 +213,8 @@ func appendV2Findings(manifest Manifest, findings []Finding) []Finding {
canary, ok := manifest.Runtime["canary"].(map[string]any) canary, ok := manifest.Runtime["canary"].(map[string]any)
if !ok || canary["status"] != "green" { if !ok || canary["status"] != "green" {
findings = append(findings, Finding{"runtime.canary", "error", "runtime.canary.status must be green before publication"}) findings = append(findings, Finding{"runtime.canary", "error", "runtime.canary.status must be green before publication"})
} else if strings.TrimSpace(toString(canary["ref"])) == "" {
findings = append(findings, Finding{"runtime.canary_ref", "error", "runtime.canary.ref must point to real canary or release evidence"})
} }
} }
if !hasRequiredKeys(manifest.InstallRequirements, []string{"tenantLoginRequired", "entitlementRequired"}) { if !hasRequiredKeys(manifest.InstallRequirements, []string{"tenantLoginRequired", "entitlementRequired"}) {
@@ -213,6 +228,63 @@ func appendV2Findings(manifest Manifest, findings []Finding) []Finding {
return findings return findings
} }
func appendPlaceholderFindings(manifest Manifest, findings []Finding) []Finding {
fields := map[string]any{
"publisher": manifest.Publisher,
"repository": manifest.Repository,
"documentation": manifest.Documentation,
"provider": manifest.Provider,
"runtime": manifest.Runtime,
}
for field, value := range fields {
if containsPlaceholder(value) {
findings = append(findings, Finding{
Code: "manifest.placeholder",
Severity: "error",
Message: field + " contains placeholder/example metadata; production connector manifests require real values",
})
}
}
return findings
}
func containsPlaceholder(value any) bool {
switch typed := value.(type) {
case string:
normalized := strings.ToLower(strings.TrimSpace(typed))
for _, marker := range []string{"change me", "change-me", "change_me", "example.com", "example.org", "example.net"} {
if strings.Contains(normalized, marker) {
return true
}
}
case map[string]string:
for _, nested := range typed {
if containsPlaceholder(nested) {
return true
}
}
case map[string]any:
for _, nested := range typed {
if containsPlaceholder(nested) {
return true
}
}
case []string:
for _, nested := range typed {
if containsPlaceholder(nested) {
return true
}
}
case []any:
for _, nested := range typed {
if containsPlaceholder(nested) {
return true
}
}
}
return false
}
func hasRequiredKeys(value map[string]any, keys []string) bool { func hasRequiredKeys(value map[string]any, keys []string) bool {
if value == nil { if value == nil {
return false return false
+54
View File
@@ -58,3 +58,57 @@ func TestValidateV2ManifestRequiresRuntimeMetadata(t *testing.T) {
t.Fatalf("missing runtime finding: %+v", result.Findings) t.Fatalf("missing runtime finding: %+v", result.Findings)
} }
} }
func TestValidateV2ManifestRejectsEmptyCanaryRef(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)
}
manifest.Runtime["canary"].(map[string]any)["ref"] = ""
result := ValidateManifest(manifest)
if result.OK {
t.Fatalf("expected invalid manifest without canary evidence ref")
}
var found bool
for _, finding := range result.Findings {
if finding.Code == "runtime.canary_ref" {
found = true
}
}
if !found {
t.Fatalf("missing canary ref finding: %+v", result.Findings)
}
}
func TestValidateV2ManifestRejectsPlaceholderMetadata(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)
}
manifest.Repository["url"] = "https://example.com/change-me/github"
result := ValidateManifest(manifest)
if result.OK {
t.Fatalf("expected invalid manifest with placeholder metadata")
}
var found bool
for _, finding := range result.Findings {
if finding.Code == "manifest.placeholder" {
found = true
}
}
if !found {
t.Fatalf("missing placeholder finding: %+v", result.Findings)
}
}