Skip to content

Commit 2e68cab

Browse files
authored
Ensure MCP servers from the registry match their expected provenance information (#1269)
1 parent 1281a0a commit 2e68cab

File tree

2 files changed

+119
-21
lines changed

2 files changed

+119
-21
lines changed

.github/workflows/update-registry.yml

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ jobs:
4545
- name: Update registry
4646
id: update
4747
run: |
48-
# Run regup with the specified count
49-
./bin/regup update --count ${{ steps.set-count.outputs.count }}
48+
# Run regup with the specified count and provenance verification
49+
./bin/regup update --count ${{ steps.set-count.outputs.count }} --verify-provenance
5050
5151
# Check if there are changes
5252
if git diff --exit-code pkg/registry/data/registry.json; then
@@ -55,6 +55,9 @@ jobs:
5555
else
5656
echo "changes=true" >> $GITHUB_OUTPUT
5757
echo "Changes detected in the registry"
58+
# Log what changed for debugging
59+
git diff --name-only --diff-filter=M pkg/registry/data/registry.json || true
60+
git diff --stat pkg/registry/data/registry.json || true
5861
fi
5962
env:
6063
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -64,12 +67,14 @@ jobs:
6467
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
6568
with:
6669
token: ${{ secrets.GITHUB_TOKEN }}
67-
commit-message: "Update registry with latest stars and pulls"
68-
title: "Update registry with latest stars and pulls"
70+
commit-message: "Refresh registry data - pulls, stars, etc."
71+
title: "Refresh registry data - pulls, stars, etc."
6972
body: |
70-
This PR updates the registry with the latest GitHub stars and pulls information.
73+
This PR refreshes the registry with the latest GitHub stars and pulls information.
7174
72-
The update was performed automatically by the `regup` command.
75+
The update was performed automatically by the `regup` command with provenance verification enabled.
76+
77+
** Note: Any servers that failed provenance verification have been automatically removed from the registry. **
7378
branch: update-registry
7479
base: main
75-
delete-branch: true
80+
delete-branch: true

cmd/regup/app/update.go

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package app
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"os"
@@ -13,22 +14,34 @@ import (
1314

1415
"github.com/spf13/cobra"
1516

17+
"github.com/stacklok/toolhive/pkg/container/verifier"
1618
"github.com/stacklok/toolhive/pkg/logger"
1719
"github.com/stacklok/toolhive/pkg/registry"
1820
)
1921

2022
var (
21-
count int
22-
dryRun bool
23-
githubToken string
24-
serverName string
23+
count int
24+
dryRun bool
25+
githubToken string
26+
serverName string
27+
verifyProvenance bool
2528
)
2629

2730
type serverWithName struct {
2831
name string
2932
server *registry.ImageMetadata
3033
}
3134

35+
// ProvenanceVerificationError represents an error during provenance verification
36+
type ProvenanceVerificationError struct {
37+
ServerName string
38+
Reason string
39+
}
40+
41+
func (e *ProvenanceVerificationError) Error() string {
42+
return fmt.Sprintf("provenance verification failed for server %s: %s", e.ServerName, e.Reason)
43+
}
44+
3245
var updateCmd = &cobra.Command{
3346
Use: "update",
3447
Short: "Update registry entries with latest information",
@@ -45,6 +58,8 @@ func init() {
4558
"GitHub token for API authentication (can also be set via GITHUB_TOKEN env var)")
4659
updateCmd.Flags().StringVarP(&serverName, "server", "s", "",
4760
"Specific server name to update")
61+
updateCmd.Flags().BoolVar(&verifyProvenance, "verify-provenance", false,
62+
"Verify provenance information and remove servers that fail verification")
4863

4964
// Mark count and server flags as mutually exclusive
5065
updateCmd.MarkFlagsMutuallyExclusive("count", "server")
@@ -69,10 +84,16 @@ func updateCmdFunc(_ *cobra.Command, _ []string) error {
6984
}
7085

7186
// Update servers
72-
updatedServers := updateServers(servers)
87+
updatedServers, failedServers := updateServers(servers, reg)
88+
89+
// Log summary
90+
if len(failedServers) > 0 {
91+
logger.Warnf("Removed %d servers due to provenance verification failures: %v",
92+
len(failedServers), failedServers)
93+
}
7394

7495
// Save results
75-
return saveResults(reg, updatedServers)
96+
return saveResults(reg, updatedServers, failedServers)
7697
}
7798

7899
func loadRegistry() (*registry.Registry, error) {
@@ -159,37 +180,49 @@ func isOlder(serverI, serverJ *registry.ImageMetadata) bool {
159180
return timeI.Before(timeJ)
160181
}
161182

162-
func updateServers(servers []serverWithName) []string {
183+
func updateServers(servers []serverWithName, reg *registry.Registry) ([]string, []string) {
163184
updatedServers := make([]string, 0, len(servers))
185+
failedServers := make([]string, 0)
164186

165187
for _, s := range servers {
166188
logger.Infof("Updating server: %s", s.name)
167189

168190
if err := updateServerInfo(s.name, s.server); err != nil {
191+
var provenanceErr *ProvenanceVerificationError
192+
if errors.As(err, &provenanceErr) {
193+
logger.Errorf("Provenance verification failed for server %s: %v", s.name, err)
194+
failedServers = append(failedServers, s.name)
195+
continue
196+
}
169197
logger.Errorf("Failed to update server %s: %v", s.name, err)
170198
continue
171199
}
172200

173201
updatedServers = append(updatedServers, s.name)
174202
}
175203

176-
return updatedServers
204+
// Remove failed servers from registry if provenance verification is enabled
205+
if verifyProvenance && len(failedServers) > 0 {
206+
removeFailedServers(reg, failedServers)
207+
}
208+
209+
return updatedServers, failedServers
177210
}
178211

179-
func saveResults(reg *registry.Registry, updatedServers []string) error {
212+
func saveResults(reg *registry.Registry, updatedServers []string, failedServers []string) error {
180213
// If we're in dry run mode, don't save changes
181214
if dryRun {
182215
logger.Info("Dry run completed, no changes made")
183216
return nil
184217
}
185218

186-
// If we updated any servers, save the registry
187-
if len(updatedServers) > 0 {
219+
// If we updated any servers or removed any servers, save the registry
220+
if len(updatedServers) > 0 || len(failedServers) > 0 {
188221
// Update the last_updated timestamp
189222
reg.LastUpdated = time.Now().UTC().Format(time.RFC3339)
190223

191224
// Save the updated registry
192-
if err := saveRegistry(reg, updatedServers); err != nil {
225+
if err := saveRegistry(reg, updatedServers, failedServers); err != nil {
193226
return fmt.Errorf("failed to save registry: %w", err)
194227
}
195228

@@ -203,6 +236,16 @@ func saveResults(reg *registry.Registry, updatedServers []string) error {
203236

204237
// updateServerInfo updates the GitHub stars and pulls for a server
205238
func updateServerInfo(name string, server *registry.ImageMetadata) error {
239+
// Verify provenance if requested
240+
if verifyProvenance {
241+
if err := verifyServerProvenance(name, server); err != nil {
242+
return &ProvenanceVerificationError{
243+
ServerName: name,
244+
Reason: err.Error(),
245+
}
246+
}
247+
}
248+
206249
// Skip if no repository URL
207250
if server.RepositoryURL == "" {
208251
logger.Warnf("ImageMetadata %s has no repository URL, skipping", name)
@@ -245,6 +288,50 @@ func updateServerInfo(name string, server *registry.ImageMetadata) error {
245288
return nil
246289
}
247290

291+
// verifyServerProvenance verifies the provenance information for a server
292+
func verifyServerProvenance(name string, server *registry.ImageMetadata) error {
293+
// Skip if no provenance information
294+
if server.Provenance == nil {
295+
logger.Warnf("Server %s has no provenance information, skipping verification", name)
296+
return nil
297+
}
298+
299+
// Skip if no image reference
300+
if server.Image == "" {
301+
return fmt.Errorf("no image reference provided")
302+
}
303+
304+
logger.Infof("Verifying provenance for server %s with image %s", name, server.Image)
305+
306+
// Create verifier
307+
v, err := verifier.New(server)
308+
if err != nil {
309+
return fmt.Errorf("failed to create verifier: %w", err)
310+
}
311+
312+
// Get verification results
313+
isVerified, err := v.VerifyServer(server.Image, server)
314+
if err != nil {
315+
return fmt.Errorf("verification failed: %w", err)
316+
}
317+
318+
// Check if we have valid verification results
319+
if isVerified {
320+
logger.Infof("Server %s verified successfully", name)
321+
return nil
322+
}
323+
324+
return fmt.Errorf("no verified signatures found")
325+
}
326+
327+
// removeFailedServers removes servers that failed provenance verification from the registry
328+
func removeFailedServers(reg *registry.Registry, failedServers []string) {
329+
for _, serverName := range failedServers {
330+
logger.Warnf("Removing server %s from registry due to provenance verification failure", serverName)
331+
delete(reg.Servers, serverName)
332+
}
333+
}
334+
248335
// extractOwnerRepo extracts the owner and repo from a GitHub repository URL
249336
func extractOwnerRepo(url string) (string, string, error) {
250337
// Remove trailing .git if present
@@ -333,7 +420,7 @@ func getGitHubRepoInfo(owner, repo, serverName string, currentPulls int) (stars
333420
}
334421

335422
// saveRegistry saves the registry to the filesystem while preserving the order of entries
336-
func saveRegistry(reg *registry.Registry, updatedServers []string) error {
423+
func saveRegistry(reg *registry.Registry, updatedServers []string, failedServers []string) error {
337424
// Find the registry file path
338425
registryPath := filepath.Join("pkg", "registry", "data", "registry.json")
339426

@@ -359,6 +446,12 @@ func saveRegistry(reg *registry.Registry, updatedServers []string) error {
359446
return fmt.Errorf("invalid servers map in registry")
360447
}
361448

449+
// Remove failed servers from the JSON
450+
for _, name := range failedServers {
451+
logger.Infof("Removing server %s from registry JSON", name)
452+
delete(serversMap, name)
453+
}
454+
362455
// Update only the servers that were modified
363456
for _, name := range updatedServers {
364457
server, ok := reg.Servers[name]
@@ -369,7 +462,7 @@ func saveRegistry(reg *registry.Registry, updatedServers []string) error {
369462
// Get the server from the original JSON
370463
serverJSON, ok := serversMap[name].(map[string]interface{})
371464
if !ok {
372-
logger.Warnf("ImageMetadata %s not found in original registry, skipping", name)
465+
logger.Warnf("Server %s not found in original registry, skipping", name)
373466
continue
374467
}
375468

0 commit comments

Comments
 (0)