A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
81
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve admin tooling

+677 -301
+23 -131
cmd/hold/scan_backfill.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "strings" 7 6 8 - "atcr.io/pkg/atproto" 9 7 "atcr.io/pkg/hold" 10 8 11 9 "github.com/spf13/cobra" 12 10 ) 13 11 14 - // Media-type fragments that identify artifact types the scanner intentionally 15 - // skips. Keep this list in sync with scanner/internal/scan/worker.go's 16 - // unscannableConfigTypes — that map keys on config media types; here we look 17 - // at *layer* media types because the backfill walks the hold's layer index 18 - // (which has manifest AT-URIs we can join against scan records). 19 - // 20 - // Detection by layer media type is reliable: helm charts always have a single 21 - // layer with media type application/vnd.cncf.helm.chart.content.v1.tar+gzip; 22 - // in-toto / DSSE attestations use distinct layer types too. 23 - var unscannableLayerMediaSubstrings = []string{ 24 - "helm.chart.content", 25 - "in-toto", 26 - "dsse.envelope", 27 - } 28 - 29 12 var scanBackfillConfigFile string 30 13 31 14 var scanBackfillCmd = &cobra.Command{ 32 15 Use: "scan-backfill", 33 - Short: "Rewrite legacy scan records to use the status field", 16 + Short: "Rewrite legacy scan records to use the status field (offline)", 34 17 Long: `Walks every io.atcr.hold.scan record on this hold and assigns a status 35 18 ("skipped" or "failed") to records that pre-date the status field. 36 19 37 - A legacy record is one with an empty status, no SBOM blob, and zero vulnerability 38 - counts. The tool inspects each record's manifest's layers to decide: 20 + A legacy record is one with an empty status, no SBOM blob, and zero 21 + vulnerability counts. Layer media types decide the rewrite: 39 22 40 - - layer media type matches helm/in-toto/DSSE → status="skipped" 41 - - everything else → status="failed" 23 + - helm.chart.content / in-toto / dsse.envelope → status="skipped" 24 + - everything else → status="failed" 25 + 26 + The tool is idempotent and preserves each record's original scannedAt. 42 27 43 - The tool is idempotent: records that already have a status are left alone. 44 - Run once per hold after upgrading.`, 28 + This subcommand opens the hold's CAR store directly, so the running hold 29 + service must be stopped first (otherwise the embedded PDS holds an exclusive 30 + lock). For zero-downtime backfill on a production hold, hit the admin 31 + endpoint POST /admin/api/scan-backfill instead.`, 45 32 Args: cobra.NoArgs, 46 33 RunE: func(cmd *cobra.Command, args []string) error { 47 34 cfg, err := hold.LoadConfig(scanBackfillConfigFile) ··· 56 43 } 57 44 defer cleanup() 58 45 59 - ri := holdPDS.RecordsIndex() 60 - if ri == nil { 61 - return fmt.Errorf("records index not available") 46 + logf := func(format string, args ...any) { 47 + fmt.Fprintf(cmd.ErrOrStderr(), " "+format+"\n", args...) 62 48 } 63 - 64 - const batchSize = 200 65 - var ( 66 - cursor string 67 - scanned int 68 - rewritten int 69 - markSkipped int 70 - markFailed int 71 - alreadyOK int 72 - ) 73 - 74 - for { 75 - records, nextCursor, err := ri.ListRecords(atproto.ScanCollection, batchSize, cursor, true) 76 - if err != nil { 77 - return fmt.Errorf("list scan records: %w", err) 78 - } 79 - 80 - for _, rec := range records { 81 - scanned++ 82 - manifestDigest := "sha256:" + rec.Rkey 83 - 84 - _, scanRecord, err := holdPDS.GetScanRecord(ctx, manifestDigest) 85 - if err != nil { 86 - fmt.Fprintf(cmd.ErrOrStderr(), " skip rkey=%s: get failed: %v\n", rec.Rkey, err) 87 - continue 88 - } 89 - 90 - // Already classified — nothing to do. 91 - if scanRecord.Status != "" { 92 - alreadyOK++ 93 - continue 94 - } 95 - 96 - // Only legacy records that signal failure (nil blob + zero 97 - // counts) are candidates. Records with real data don't need 98 - // rewriting; their absent status will be treated as "ok". 99 - if scanRecord.SbomBlob != nil || scanRecord.Total != 0 { 100 - alreadyOK++ 101 - continue 102 - } 103 - 104 - // Determine artifact type from layer media types. 105 - layers, err := holdPDS.ListLayerRecordsForManifest(ctx, scanRecord.Manifest) 106 - if err != nil { 107 - fmt.Fprintf(cmd.ErrOrStderr(), " skip rkey=%s: list layers failed: %v\n", rec.Rkey, err) 108 - continue 109 - } 110 - 111 - skipped := false 112 - for _, l := range layers { 113 - for _, frag := range unscannableLayerMediaSubstrings { 114 - if strings.Contains(l.MediaType, frag) { 115 - skipped = true 116 - break 117 - } 118 - } 119 - if skipped { 120 - break 121 - } 122 - } 123 - 124 - var rewrite *atproto.ScanRecord 125 - if skipped { 126 - rewrite = atproto.NewSkippedScanRecord( 127 - manifestDigest, 128 - scanRecord.Repository, 129 - scanRecord.UserDID, 130 - "backfilled: unscannable artifact type", 131 - scanRecord.ScannerVersion, 132 - ) 133 - markSkipped++ 134 - } else { 135 - rewrite = atproto.NewFailedScanRecord( 136 - manifestDigest, 137 - scanRecord.Repository, 138 - scanRecord.UserDID, 139 - "backfilled: legacy record (no SBOM and zero counts)", 140 - scanRecord.ScannerVersion, 141 - ) 142 - markFailed++ 143 - } 144 - // Preserve the original ScannedAt — rewriting it would either 145 - // reset the rescan timer or invalidate audit signals. 146 - if scanRecord.ScannedAt != "" { 147 - rewrite.ScannedAt = scanRecord.ScannedAt 148 - } 149 - 150 - if _, _, err := holdPDS.CreateScanRecord(ctx, rewrite); err != nil { 151 - fmt.Fprintf(cmd.ErrOrStderr(), " rewrite rkey=%s failed: %v\n", rec.Rkey, err) 152 - continue 153 - } 154 - rewritten++ 155 - } 156 - 157 - if nextCursor == "" || len(records) == 0 { 158 - break 159 - } 160 - cursor = nextCursor 49 + res, err := holdPDS.BackfillScanStatus(ctx, logf, nil) 50 + if err != nil { 51 + return fmt.Errorf("backfill: %w", err) 161 52 } 162 53 163 - fmt.Fprintf(cmd.OutOrStdout(), "Backfill complete:\n") 164 - fmt.Fprintf(cmd.OutOrStdout(), " scanned: %d\n", scanned) 165 - fmt.Fprintf(cmd.OutOrStdout(), " already-tagged: %d\n", alreadyOK) 166 - fmt.Fprintf(cmd.OutOrStdout(), " → skipped: %d\n", markSkipped) 167 - fmt.Fprintf(cmd.OutOrStdout(), " → failed: %d\n", markFailed) 168 - fmt.Fprintf(cmd.OutOrStdout(), " rewritten: %d\n", rewritten) 54 + out := cmd.OutOrStdout() 55 + fmt.Fprintf(out, "Backfill complete:\n") 56 + fmt.Fprintf(out, " scanned: %d\n", res.Scanned) 57 + fmt.Fprintf(out, " already-tagged: %d\n", res.AlreadyTagged) 58 + fmt.Fprintf(out, " → skipped: %d\n", res.MarkedSkipped) 59 + fmt.Fprintf(out, " → failed: %d\n", res.MarkedFailed) 60 + fmt.Fprintf(out, " rewritten: %d\n", res.Rewritten) 169 61 return nil 170 62 }, 171 63 }
+1 -1
lexicons/io/atcr/hold/image/config.json
··· 18 18 "configJson": { 19 19 "type": "string", 20 20 "description": "Raw OCI image config JSON blob", 21 - "maxLength": 65536 21 + "maxLength": 1000000 22 22 }, 23 23 "createdAt": { 24 24 "type": "string",
+1 -1
pkg/appview/templates/partials/image-advisor-results.html
··· 2 2 {{ if eq .Error "upgrade_required" }} 3 3 <div class="alert alert-info text-sm"> 4 4 {{ icon "sparkles" "size-4" }} 5 - <span>AI Image Advisor is a paid feature. <a href="/settings/billing" class="link link-primary font-medium">Upgrade your plan</a> to unlock image analysis.</span> 5 + <span>AI Image Advisor is a paid feature. <a href="/settings/billing" class="link link-hover font-semibold underline">Upgrade your plan</a> to unlock image analysis.</span> 6 6 </div> 7 7 {{ else if .Error }} 8 8 <div class="alert alert-warning text-sm">
+163 -163
pkg/atproto/cbor_gen.go
··· 37 37 } 38 38 39 39 // t.Role (string) (string) 40 - if len("role") > 8192 { 40 + if len("role") > 1000000 { 41 41 return xerrors.Errorf("Value in field \"role\" was too long") 42 42 } 43 43 ··· 48 48 return err 49 49 } 50 50 51 - if len(t.Role) > 8192 { 51 + if len(t.Role) > 1000000 { 52 52 return xerrors.Errorf("Value in field t.Role was too long") 53 53 } 54 54 ··· 62 62 // t.Tier (string) (string) 63 63 if t.Tier != "" { 64 64 65 - if len("tier") > 8192 { 65 + if len("tier") > 1000000 { 66 66 return xerrors.Errorf("Value in field \"tier\" was too long") 67 67 } 68 68 ··· 73 73 return err 74 74 } 75 75 76 - if len(t.Tier) > 8192 { 76 + if len(t.Tier) > 1000000 { 77 77 return xerrors.Errorf("Value in field t.Tier was too long") 78 78 } 79 79 ··· 86 86 } 87 87 88 88 // t.Type (string) (string) 89 - if len("$type") > 8192 { 89 + if len("$type") > 1000000 { 90 90 return xerrors.Errorf("Value in field \"$type\" was too long") 91 91 } 92 92 ··· 97 97 return err 98 98 } 99 99 100 - if len(t.Type) > 8192 { 100 + if len(t.Type) > 1000000 { 101 101 return xerrors.Errorf("Value in field t.Type was too long") 102 102 } 103 103 ··· 109 109 } 110 110 111 111 // t.Member (string) (string) 112 - if len("member") > 8192 { 112 + if len("member") > 1000000 { 113 113 return xerrors.Errorf("Value in field \"member\" was too long") 114 114 } 115 115 ··· 120 120 return err 121 121 } 122 122 123 - if len(t.Member) > 8192 { 123 + if len(t.Member) > 1000000 { 124 124 return xerrors.Errorf("Value in field t.Member was too long") 125 125 } 126 126 ··· 132 132 } 133 133 134 134 // t.AddedAt (string) (string) 135 - if len("addedAt") > 8192 { 135 + if len("addedAt") > 1000000 { 136 136 return xerrors.Errorf("Value in field \"addedAt\" was too long") 137 137 } 138 138 ··· 143 143 return err 144 144 } 145 145 146 - if len(t.AddedAt) > 8192 { 146 + if len(t.AddedAt) > 1000000 { 147 147 return xerrors.Errorf("Value in field t.AddedAt was too long") 148 148 } 149 149 ··· 155 155 } 156 156 157 157 // t.Plankowner (bool) (bool) 158 - if len("plankowner") > 8192 { 158 + if len("plankowner") > 1000000 { 159 159 return xerrors.Errorf("Value in field \"plankowner\" was too long") 160 160 } 161 161 ··· 171 171 } 172 172 173 173 // t.Permissions ([]string) (slice) 174 - if len("permissions") > 8192 { 174 + if len("permissions") > 1000000 { 175 175 return xerrors.Errorf("Value in field \"permissions\" was too long") 176 176 } 177 177 ··· 190 190 return err 191 191 } 192 192 for _, v := range t.Permissions { 193 - if len(v) > 8192 { 193 + if len(v) > 1000000 { 194 194 return xerrors.Errorf("Value in field v was too long") 195 195 } 196 196 ··· 232 232 233 233 nameBuf := make([]byte, 11) 234 234 for i := uint64(0); i < n; i++ { 235 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 235 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 236 236 if err != nil { 237 237 return err 238 238 } ··· 250 250 case "role": 251 251 252 252 { 253 - sval, err := cbg.ReadStringWithMax(cr, 8192) 253 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 254 254 if err != nil { 255 255 return err 256 256 } ··· 261 261 case "tier": 262 262 263 263 { 264 - sval, err := cbg.ReadStringWithMax(cr, 8192) 264 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 265 265 if err != nil { 266 266 return err 267 267 } ··· 272 272 case "$type": 273 273 274 274 { 275 - sval, err := cbg.ReadStringWithMax(cr, 8192) 275 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 276 276 if err != nil { 277 277 return err 278 278 } ··· 283 283 case "member": 284 284 285 285 { 286 - sval, err := cbg.ReadStringWithMax(cr, 8192) 286 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 287 287 if err != nil { 288 288 return err 289 289 } ··· 294 294 case "addedAt": 295 295 296 296 { 297 - sval, err := cbg.ReadStringWithMax(cr, 8192) 297 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 298 298 if err != nil { 299 299 return err 300 300 } ··· 349 349 _ = err 350 350 351 351 { 352 - sval, err := cbg.ReadStringWithMax(cr, 8192) 352 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 353 353 if err != nil { 354 354 return err 355 355 } ··· 392 392 } 393 393 394 394 // t.Type (string) (string) 395 - if len("$type") > 8192 { 395 + if len("$type") > 1000000 { 396 396 return xerrors.Errorf("Value in field \"$type\" was too long") 397 397 } 398 398 ··· 403 403 return err 404 404 } 405 405 406 - if len(t.Type) > 8192 { 406 + if len(t.Type) > 1000000 { 407 407 return xerrors.Errorf("Value in field t.Type was too long") 408 408 } 409 409 ··· 415 415 } 416 416 417 417 // t.Owner (string) (string) 418 - if len("owner") > 8192 { 418 + if len("owner") > 1000000 { 419 419 return xerrors.Errorf("Value in field \"owner\" was too long") 420 420 } 421 421 ··· 426 426 return err 427 427 } 428 428 429 - if len(t.Owner) > 8192 { 429 + if len(t.Owner) > 1000000 { 430 430 return xerrors.Errorf("Value in field t.Owner was too long") 431 431 } 432 432 ··· 438 438 } 439 439 440 440 // t.Public (bool) (bool) 441 - if len("public") > 8192 { 441 + if len("public") > 1000000 { 442 442 return xerrors.Errorf("Value in field \"public\" was too long") 443 443 } 444 444 ··· 456 456 // t.Region (string) (string) 457 457 if t.Region != "" { 458 458 459 - if len("region") > 8192 { 459 + if len("region") > 1000000 { 460 460 return xerrors.Errorf("Value in field \"region\" was too long") 461 461 } 462 462 ··· 467 467 return err 468 468 } 469 469 470 - if len(t.Region) > 8192 { 470 + if len(t.Region) > 1000000 { 471 471 return xerrors.Errorf("Value in field t.Region was too long") 472 472 } 473 473 ··· 482 482 // t.Successor (string) (string) 483 483 if t.Successor != "" { 484 484 485 - if len("successor") > 8192 { 485 + if len("successor") > 1000000 { 486 486 return xerrors.Errorf("Value in field \"successor\" was too long") 487 487 } 488 488 ··· 493 493 return err 494 494 } 495 495 496 - if len(t.Successor) > 8192 { 496 + if len(t.Successor) > 1000000 { 497 497 return xerrors.Errorf("Value in field t.Successor was too long") 498 498 } 499 499 ··· 506 506 } 507 507 508 508 // t.DeployedAt (string) (string) 509 - if len("deployedAt") > 8192 { 509 + if len("deployedAt") > 1000000 { 510 510 return xerrors.Errorf("Value in field \"deployedAt\" was too long") 511 511 } 512 512 ··· 517 517 return err 518 518 } 519 519 520 - if len(t.DeployedAt) > 8192 { 520 + if len(t.DeployedAt) > 1000000 { 521 521 return xerrors.Errorf("Value in field t.DeployedAt was too long") 522 522 } 523 523 ··· 529 529 } 530 530 531 531 // t.AllowAllCrew (bool) (bool) 532 - if len("allowAllCrew") > 8192 { 532 + if len("allowAllCrew") > 1000000 { 533 533 return xerrors.Errorf("Value in field \"allowAllCrew\" was too long") 534 534 } 535 535 ··· 545 545 } 546 546 547 547 // t.EnableBlueskyPosts (bool) (bool) 548 - if len("enableBlueskyPosts") > 8192 { 548 + if len("enableBlueskyPosts") > 1000000 { 549 549 return xerrors.Errorf("Value in field \"enableBlueskyPosts\" was too long") 550 550 } 551 551 ··· 589 589 590 590 nameBuf := make([]byte, 18) 591 591 for i := uint64(0); i < n; i++ { 592 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 592 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 593 593 if err != nil { 594 594 return err 595 595 } ··· 607 607 case "$type": 608 608 609 609 { 610 - sval, err := cbg.ReadStringWithMax(cr, 8192) 610 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 611 611 if err != nil { 612 612 return err 613 613 } ··· 618 618 case "owner": 619 619 620 620 { 621 - sval, err := cbg.ReadStringWithMax(cr, 8192) 621 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 622 622 if err != nil { 623 623 return err 624 624 } ··· 647 647 case "region": 648 648 649 649 { 650 - sval, err := cbg.ReadStringWithMax(cr, 8192) 650 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 651 651 if err != nil { 652 652 return err 653 653 } ··· 658 658 case "successor": 659 659 660 660 { 661 - sval, err := cbg.ReadStringWithMax(cr, 8192) 661 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 662 662 if err != nil { 663 663 return err 664 664 } ··· 669 669 case "deployedAt": 670 670 671 671 { 672 - sval, err := cbg.ReadStringWithMax(cr, 8192) 672 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 673 673 if err != nil { 674 674 return err 675 675 } ··· 736 736 } 737 737 738 738 // t.Size (int64) (int64) 739 - if len("size") > 8192 { 739 + if len("size") > 1000000 { 740 740 return xerrors.Errorf("Value in field \"size\" was too long") 741 741 } 742 742 ··· 758 758 } 759 759 760 760 // t.Type (string) (string) 761 - if len("$type") > 8192 { 761 + if len("$type") > 1000000 { 762 762 return xerrors.Errorf("Value in field \"$type\" was too long") 763 763 } 764 764 ··· 769 769 return err 770 770 } 771 771 772 - if len(t.Type) > 8192 { 772 + if len(t.Type) > 1000000 { 773 773 return xerrors.Errorf("Value in field t.Type was too long") 774 774 } 775 775 ··· 781 781 } 782 782 783 783 // t.Digest (string) (string) 784 - if len("digest") > 8192 { 784 + if len("digest") > 1000000 { 785 785 return xerrors.Errorf("Value in field \"digest\" was too long") 786 786 } 787 787 ··· 792 792 return err 793 793 } 794 794 795 - if len(t.Digest) > 8192 { 795 + if len(t.Digest) > 1000000 { 796 796 return xerrors.Errorf("Value in field t.Digest was too long") 797 797 } 798 798 ··· 804 804 } 805 805 806 806 // t.UserDID (string) (string) 807 - if len("userDid") > 8192 { 807 + if len("userDid") > 1000000 { 808 808 return xerrors.Errorf("Value in field \"userDid\" was too long") 809 809 } 810 810 ··· 815 815 return err 816 816 } 817 817 818 - if len(t.UserDID) > 8192 { 818 + if len(t.UserDID) > 1000000 { 819 819 return xerrors.Errorf("Value in field t.UserDID was too long") 820 820 } 821 821 ··· 827 827 } 828 828 829 829 // t.Manifest (string) (string) 830 - if len("manifest") > 8192 { 830 + if len("manifest") > 1000000 { 831 831 return xerrors.Errorf("Value in field \"manifest\" was too long") 832 832 } 833 833 ··· 838 838 return err 839 839 } 840 840 841 - if len(t.Manifest) > 8192 { 841 + if len(t.Manifest) > 1000000 { 842 842 return xerrors.Errorf("Value in field t.Manifest was too long") 843 843 } 844 844 ··· 850 850 } 851 851 852 852 // t.CreatedAt (string) (string) 853 - if len("createdAt") > 8192 { 853 + if len("createdAt") > 1000000 { 854 854 return xerrors.Errorf("Value in field \"createdAt\" was too long") 855 855 } 856 856 ··· 861 861 return err 862 862 } 863 863 864 - if len(t.CreatedAt) > 8192 { 864 + if len(t.CreatedAt) > 1000000 { 865 865 return xerrors.Errorf("Value in field t.CreatedAt was too long") 866 866 } 867 867 ··· 873 873 } 874 874 875 875 // t.MediaType (string) (string) 876 - if len("mediaType") > 8192 { 876 + if len("mediaType") > 1000000 { 877 877 return xerrors.Errorf("Value in field \"mediaType\" was too long") 878 878 } 879 879 ··· 884 884 return err 885 885 } 886 886 887 - if len(t.MediaType) > 8192 { 887 + if len(t.MediaType) > 1000000 { 888 888 return xerrors.Errorf("Value in field t.MediaType was too long") 889 889 } 890 890 ··· 924 924 925 925 nameBuf := make([]byte, 9) 926 926 for i := uint64(0); i < n; i++ { 927 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 927 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 928 928 if err != nil { 929 929 return err 930 930 } ··· 968 968 case "$type": 969 969 970 970 { 971 - sval, err := cbg.ReadStringWithMax(cr, 8192) 971 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 972 972 if err != nil { 973 973 return err 974 974 } ··· 979 979 case "digest": 980 980 981 981 { 982 - sval, err := cbg.ReadStringWithMax(cr, 8192) 982 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 983 983 if err != nil { 984 984 return err 985 985 } ··· 990 990 case "userDid": 991 991 992 992 { 993 - sval, err := cbg.ReadStringWithMax(cr, 8192) 993 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 994 994 if err != nil { 995 995 return err 996 996 } ··· 1001 1001 case "manifest": 1002 1002 1003 1003 { 1004 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1004 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1005 1005 if err != nil { 1006 1006 return err 1007 1007 } ··· 1012 1012 case "createdAt": 1013 1013 1014 1014 { 1015 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1015 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1016 1016 if err != nil { 1017 1017 return err 1018 1018 } ··· 1023 1023 case "mediaType": 1024 1024 1025 1025 { 1026 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1026 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1027 1027 if err != nil { 1028 1028 return err 1029 1029 } ··· 1054 1054 } 1055 1055 1056 1056 // t.Type (string) (string) 1057 - if len("$type") > 8192 { 1057 + if len("$type") > 1000000 { 1058 1058 return xerrors.Errorf("Value in field \"$type\" was too long") 1059 1059 } 1060 1060 ··· 1065 1065 return err 1066 1066 } 1067 1067 1068 - if len(t.Type) > 8192 { 1068 + if len(t.Type) > 1000000 { 1069 1069 return xerrors.Errorf("Value in field t.Type was too long") 1070 1070 } 1071 1071 ··· 1077 1077 } 1078 1078 1079 1079 // t.Links ([]string) (slice) 1080 - if len("links") > 8192 { 1080 + if len("links") > 1000000 { 1081 1081 return xerrors.Errorf("Value in field \"links\" was too long") 1082 1082 } 1083 1083 ··· 1096 1096 return err 1097 1097 } 1098 1098 for _, v := range t.Links { 1099 - if len(v) > 8192 { 1099 + if len(v) > 1000000 { 1100 1100 return xerrors.Errorf("Value in field v was too long") 1101 1101 } 1102 1102 ··· 1110 1110 } 1111 1111 1112 1112 // t.Stats ([]string) (slice) 1113 - if len("stats") > 8192 { 1113 + if len("stats") > 1000000 { 1114 1114 return xerrors.Errorf("Value in field \"stats\" was too long") 1115 1115 } 1116 1116 ··· 1129 1129 return err 1130 1130 } 1131 1131 for _, v := range t.Stats { 1132 - if len(v) > 8192 { 1132 + if len(v) > 1000000 { 1133 1133 return xerrors.Errorf("Value in field v was too long") 1134 1134 } 1135 1135 ··· 1143 1143 } 1144 1144 1145 1145 // t.Bluesky (bool) (bool) 1146 - if len("bluesky") > 8192 { 1146 + if len("bluesky") > 1000000 { 1147 1147 return xerrors.Errorf("Value in field \"bluesky\" was too long") 1148 1148 } 1149 1149 ··· 1159 1159 } 1160 1160 1161 1161 // t.Location (string) (string) 1162 - if len("location") > 8192 { 1162 + if len("location") > 1000000 { 1163 1163 return xerrors.Errorf("Value in field \"location\" was too long") 1164 1164 } 1165 1165 ··· 1170 1170 return err 1171 1171 } 1172 1172 1173 - if len(t.Location) > 8192 { 1173 + if len(t.Location) > 1000000 { 1174 1174 return xerrors.Errorf("Value in field t.Location was too long") 1175 1175 } 1176 1176 ··· 1182 1182 } 1183 1183 1184 1184 // t.Description (string) (string) 1185 - if len("description") > 8192 { 1185 + if len("description") > 1000000 { 1186 1186 return xerrors.Errorf("Value in field \"description\" was too long") 1187 1187 } 1188 1188 ··· 1193 1193 return err 1194 1194 } 1195 1195 1196 - if len(t.Description) > 8192 { 1196 + if len(t.Description) > 1000000 { 1197 1197 return xerrors.Errorf("Value in field t.Description was too long") 1198 1198 } 1199 1199 ··· 1205 1205 } 1206 1206 1207 1207 // t.PinnedRepositories ([]string) (slice) 1208 - if len("pinnedRepositories") > 8192 { 1208 + if len("pinnedRepositories") > 1000000 { 1209 1209 return xerrors.Errorf("Value in field \"pinnedRepositories\" was too long") 1210 1210 } 1211 1211 ··· 1224 1224 return err 1225 1225 } 1226 1226 for _, v := range t.PinnedRepositories { 1227 - if len(v) > 8192 { 1227 + if len(v) > 1000000 { 1228 1228 return xerrors.Errorf("Value in field v was too long") 1229 1229 } 1230 1230 ··· 1266 1266 1267 1267 nameBuf := make([]byte, 18) 1268 1268 for i := uint64(0); i < n; i++ { 1269 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 1269 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1270 1270 if err != nil { 1271 1271 return err 1272 1272 } ··· 1284 1284 case "$type": 1285 1285 1286 1286 { 1287 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1287 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1288 1288 if err != nil { 1289 1289 return err 1290 1290 } ··· 1321 1321 _ = err 1322 1322 1323 1323 { 1324 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1324 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1325 1325 if err != nil { 1326 1326 return err 1327 1327 } ··· 1361 1361 _ = err 1362 1362 1363 1363 { 1364 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1364 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1365 1365 if err != nil { 1366 1366 return err 1367 1367 } ··· 1393 1393 case "location": 1394 1394 1395 1395 { 1396 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1396 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1397 1397 if err != nil { 1398 1398 return err 1399 1399 } ··· 1404 1404 case "description": 1405 1405 1406 1406 { 1407 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1407 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1408 1408 if err != nil { 1409 1409 return err 1410 1410 } ··· 1441 1441 _ = err 1442 1442 1443 1443 { 1444 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1444 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1445 1445 if err != nil { 1446 1446 return err 1447 1447 } ··· 1484 1484 } 1485 1485 1486 1486 // t.Type (string) (string) 1487 - if len("$type") > 8192 { 1487 + if len("$type") > 1000000 { 1488 1488 return xerrors.Errorf("Value in field \"$type\" was too long") 1489 1489 } 1490 1490 ··· 1495 1495 return err 1496 1496 } 1497 1497 1498 - if len(t.Type) > 8192 { 1498 + if len(t.Type) > 1000000 { 1499 1499 return xerrors.Errorf("Value in field t.Type was too long") 1500 1500 } 1501 1501 ··· 1509 1509 // t.LastPull (string) (string) 1510 1510 if t.LastPull != "" { 1511 1511 1512 - if len("lastPull") > 8192 { 1512 + if len("lastPull") > 1000000 { 1513 1513 return xerrors.Errorf("Value in field \"lastPull\" was too long") 1514 1514 } 1515 1515 ··· 1520 1520 return err 1521 1521 } 1522 1522 1523 - if len(t.LastPull) > 8192 { 1523 + if len(t.LastPull) > 1000000 { 1524 1524 return xerrors.Errorf("Value in field t.LastPull was too long") 1525 1525 } 1526 1526 ··· 1535 1535 // t.LastPush (string) (string) 1536 1536 if t.LastPush != "" { 1537 1537 1538 - if len("lastPush") > 8192 { 1538 + if len("lastPush") > 1000000 { 1539 1539 return xerrors.Errorf("Value in field \"lastPush\" was too long") 1540 1540 } 1541 1541 ··· 1546 1546 return err 1547 1547 } 1548 1548 1549 - if len(t.LastPush) > 8192 { 1549 + if len(t.LastPush) > 1000000 { 1550 1550 return xerrors.Errorf("Value in field t.LastPush was too long") 1551 1551 } 1552 1552 ··· 1559 1559 } 1560 1560 1561 1561 // t.OwnerDID (string) (string) 1562 - if len("ownerDid") > 8192 { 1562 + if len("ownerDid") > 1000000 { 1563 1563 return xerrors.Errorf("Value in field \"ownerDid\" was too long") 1564 1564 } 1565 1565 ··· 1570 1570 return err 1571 1571 } 1572 1572 1573 - if len(t.OwnerDID) > 8192 { 1573 + if len(t.OwnerDID) > 1000000 { 1574 1574 return xerrors.Errorf("Value in field t.OwnerDID was too long") 1575 1575 } 1576 1576 ··· 1582 1582 } 1583 1583 1584 1584 // t.PullCount (int64) (int64) 1585 - if len("pullCount") > 8192 { 1585 + if len("pullCount") > 1000000 { 1586 1586 return xerrors.Errorf("Value in field \"pullCount\" was too long") 1587 1587 } 1588 1588 ··· 1604 1604 } 1605 1605 1606 1606 // t.PushCount (int64) (int64) 1607 - if len("pushCount") > 8192 { 1607 + if len("pushCount") > 1000000 { 1608 1608 return xerrors.Errorf("Value in field \"pushCount\" was too long") 1609 1609 } 1610 1610 ··· 1626 1626 } 1627 1627 1628 1628 // t.UpdatedAt (string) (string) 1629 - if len("updatedAt") > 8192 { 1629 + if len("updatedAt") > 1000000 { 1630 1630 return xerrors.Errorf("Value in field \"updatedAt\" was too long") 1631 1631 } 1632 1632 ··· 1637 1637 return err 1638 1638 } 1639 1639 1640 - if len(t.UpdatedAt) > 8192 { 1640 + if len(t.UpdatedAt) > 1000000 { 1641 1641 return xerrors.Errorf("Value in field t.UpdatedAt was too long") 1642 1642 } 1643 1643 ··· 1649 1649 } 1650 1650 1651 1651 // t.Repository (string) (string) 1652 - if len("repository") > 8192 { 1652 + if len("repository") > 1000000 { 1653 1653 return xerrors.Errorf("Value in field \"repository\" was too long") 1654 1654 } 1655 1655 ··· 1660 1660 return err 1661 1661 } 1662 1662 1663 - if len(t.Repository) > 8192 { 1663 + if len(t.Repository) > 1000000 { 1664 1664 return xerrors.Errorf("Value in field t.Repository was too long") 1665 1665 } 1666 1666 ··· 1700 1700 1701 1701 nameBuf := make([]byte, 10) 1702 1702 for i := uint64(0); i < n; i++ { 1703 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 1703 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 1704 1704 if err != nil { 1705 1705 return err 1706 1706 } ··· 1718 1718 case "$type": 1719 1719 1720 1720 { 1721 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1721 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1722 1722 if err != nil { 1723 1723 return err 1724 1724 } ··· 1729 1729 case "lastPull": 1730 1730 1731 1731 { 1732 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1732 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1733 1733 if err != nil { 1734 1734 return err 1735 1735 } ··· 1740 1740 case "lastPush": 1741 1741 1742 1742 { 1743 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1743 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1744 1744 if err != nil { 1745 1745 return err 1746 1746 } ··· 1751 1751 case "ownerDid": 1752 1752 1753 1753 { 1754 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1754 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1755 1755 if err != nil { 1756 1756 return err 1757 1757 } ··· 1814 1814 case "updatedAt": 1815 1815 1816 1816 { 1817 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1817 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1818 1818 if err != nil { 1819 1819 return err 1820 1820 } ··· 1825 1825 case "repository": 1826 1826 1827 1827 { 1828 - sval, err := cbg.ReadStringWithMax(cr, 8192) 1828 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 1829 1829 if err != nil { 1830 1830 return err 1831 1831 } ··· 1856 1856 } 1857 1857 1858 1858 // t.Date (string) (string) 1859 - if len("date") > 8192 { 1859 + if len("date") > 1000000 { 1860 1860 return xerrors.Errorf("Value in field \"date\" was too long") 1861 1861 } 1862 1862 ··· 1867 1867 return err 1868 1868 } 1869 1869 1870 - if len(t.Date) > 8192 { 1870 + if len(t.Date) > 1000000 { 1871 1871 return xerrors.Errorf("Value in field t.Date was too long") 1872 1872 } 1873 1873 ··· 1879 1879 } 1880 1880 1881 1881 // t.Type (string) (string) 1882 - if len("$type") > 8192 { 1882 + if len("$type") > 1000000 { 1883 1883 return xerrors.Errorf("Value in field \"$type\" was too long") 1884 1884 } 1885 1885 ··· 1890 1890 return err 1891 1891 } 1892 1892 1893 - if len(t.Type) > 8192 { 1893 + if len(t.Type) > 1000000 { 1894 1894 return xerrors.Errorf("Value in field t.Type was too long") 1895 1895 } 1896 1896 ··· 1902 1902 } 1903 1903 1904 1904 // t.OwnerDID (string) (string) 1905 - if len("ownerDid") > 8192 { 1905 + if len("ownerDid") > 1000000 { 1906 1906 return xerrors.Errorf("Value in field \"ownerDid\" was too long") 1907 1907 } 1908 1908 ··· 1913 1913 return err 1914 1914 } 1915 1915 1916 - if len(t.OwnerDID) > 8192 { 1916 + if len(t.OwnerDID) > 1000000 { 1917 1917 return xerrors.Errorf("Value in field t.OwnerDID was too long") 1918 1918 } 1919 1919 ··· 1925 1925 } 1926 1926 1927 1927 // t.PullCount (int64) (int64) 1928 - if len("pullCount") > 8192 { 1928 + if len("pullCount") > 1000000 { 1929 1929 return xerrors.Errorf("Value in field \"pullCount\" was too long") 1930 1930 } 1931 1931 ··· 1947 1947 } 1948 1948 1949 1949 // t.PushCount (int64) (int64) 1950 - if len("pushCount") > 8192 { 1950 + if len("pushCount") > 1000000 { 1951 1951 return xerrors.Errorf("Value in field \"pushCount\" was too long") 1952 1952 } 1953 1953 ··· 1969 1969 } 1970 1970 1971 1971 // t.UpdatedAt (string) (string) 1972 - if len("updatedAt") > 8192 { 1972 + if len("updatedAt") > 1000000 { 1973 1973 return xerrors.Errorf("Value in field \"updatedAt\" was too long") 1974 1974 } 1975 1975 ··· 1980 1980 return err 1981 1981 } 1982 1982 1983 - if len(t.UpdatedAt) > 8192 { 1983 + if len(t.UpdatedAt) > 1000000 { 1984 1984 return xerrors.Errorf("Value in field t.UpdatedAt was too long") 1985 1985 } 1986 1986 ··· 1992 1992 } 1993 1993 1994 1994 // t.Repository (string) (string) 1995 - if len("repository") > 8192 { 1995 + if len("repository") > 1000000 { 1996 1996 return xerrors.Errorf("Value in field \"repository\" was too long") 1997 1997 } 1998 1998 ··· 2003 2003 return err 2004 2004 } 2005 2005 2006 - if len(t.Repository) > 8192 { 2006 + if len(t.Repository) > 1000000 { 2007 2007 return xerrors.Errorf("Value in field t.Repository was too long") 2008 2008 } 2009 2009 ··· 2043 2043 2044 2044 nameBuf := make([]byte, 10) 2045 2045 for i := uint64(0); i < n; i++ { 2046 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 2046 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2047 2047 if err != nil { 2048 2048 return err 2049 2049 } ··· 2061 2061 case "date": 2062 2062 2063 2063 { 2064 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2064 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2065 2065 if err != nil { 2066 2066 return err 2067 2067 } ··· 2072 2072 case "$type": 2073 2073 2074 2074 { 2075 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2075 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2076 2076 if err != nil { 2077 2077 return err 2078 2078 } ··· 2083 2083 case "ownerDid": 2084 2084 2085 2085 { 2086 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2086 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2087 2087 if err != nil { 2088 2088 return err 2089 2089 } ··· 2146 2146 case "updatedAt": 2147 2147 2148 2148 { 2149 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2149 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2150 2150 if err != nil { 2151 2151 return err 2152 2152 } ··· 2157 2157 case "repository": 2158 2158 2159 2159 { 2160 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2160 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2161 2161 if err != nil { 2162 2162 return err 2163 2163 } ··· 2197 2197 } 2198 2198 2199 2199 // t.Low (int64) (int64) 2200 - if len("low") > 8192 { 2200 + if len("low") > 1000000 { 2201 2201 return xerrors.Errorf("Value in field \"low\" was too long") 2202 2202 } 2203 2203 ··· 2219 2219 } 2220 2220 2221 2221 // t.High (int64) (int64) 2222 - if len("high") > 8192 { 2222 + if len("high") > 1000000 { 2223 2223 return xerrors.Errorf("Value in field \"high\" was too long") 2224 2224 } 2225 2225 ··· 2241 2241 } 2242 2242 2243 2243 // t.Type (string) (string) 2244 - if len("$type") > 8192 { 2244 + if len("$type") > 1000000 { 2245 2245 return xerrors.Errorf("Value in field \"$type\" was too long") 2246 2246 } 2247 2247 ··· 2252 2252 return err 2253 2253 } 2254 2254 2255 - if len(t.Type) > 8192 { 2255 + if len(t.Type) > 1000000 { 2256 2256 return xerrors.Errorf("Value in field t.Type was too long") 2257 2257 } 2258 2258 ··· 2264 2264 } 2265 2265 2266 2266 // t.Total (int64) (int64) 2267 - if len("total") > 8192 { 2267 + if len("total") > 1000000 { 2268 2268 return xerrors.Errorf("Value in field \"total\" was too long") 2269 2269 } 2270 2270 ··· 2286 2286 } 2287 2287 2288 2288 // t.Medium (int64) (int64) 2289 - if len("medium") > 8192 { 2289 + if len("medium") > 1000000 { 2290 2290 return xerrors.Errorf("Value in field \"medium\" was too long") 2291 2291 } 2292 2292 ··· 2310 2310 // t.Reason (string) (string) 2311 2311 if t.Reason != "" { 2312 2312 2313 - if len("reason") > 8192 { 2313 + if len("reason") > 1000000 { 2314 2314 return xerrors.Errorf("Value in field \"reason\" was too long") 2315 2315 } 2316 2316 ··· 2321 2321 return err 2322 2322 } 2323 2323 2324 - if len(t.Reason) > 8192 { 2324 + if len(t.Reason) > 1000000 { 2325 2325 return xerrors.Errorf("Value in field t.Reason was too long") 2326 2326 } 2327 2327 ··· 2336 2336 // t.Status (string) (string) 2337 2337 if t.Status != "" { 2338 2338 2339 - if len("status") > 8192 { 2339 + if len("status") > 1000000 { 2340 2340 return xerrors.Errorf("Value in field \"status\" was too long") 2341 2341 } 2342 2342 ··· 2347 2347 return err 2348 2348 } 2349 2349 2350 - if len(t.Status) > 8192 { 2350 + if len(t.Status) > 1000000 { 2351 2351 return xerrors.Errorf("Value in field t.Status was too long") 2352 2352 } 2353 2353 ··· 2360 2360 } 2361 2361 2362 2362 // t.UserDID (string) (string) 2363 - if len("userDid") > 8192 { 2363 + if len("userDid") > 1000000 { 2364 2364 return xerrors.Errorf("Value in field \"userDid\" was too long") 2365 2365 } 2366 2366 ··· 2371 2371 return err 2372 2372 } 2373 2373 2374 - if len(t.UserDID) > 8192 { 2374 + if len(t.UserDID) > 1000000 { 2375 2375 return xerrors.Errorf("Value in field t.UserDID was too long") 2376 2376 } 2377 2377 ··· 2383 2383 } 2384 2384 2385 2385 // t.Critical (int64) (int64) 2386 - if len("critical") > 8192 { 2386 + if len("critical") > 1000000 { 2387 2387 return xerrors.Errorf("Value in field \"critical\" was too long") 2388 2388 } 2389 2389 ··· 2405 2405 } 2406 2406 2407 2407 // t.Manifest (string) (string) 2408 - if len("manifest") > 8192 { 2408 + if len("manifest") > 1000000 { 2409 2409 return xerrors.Errorf("Value in field \"manifest\" was too long") 2410 2410 } 2411 2411 ··· 2416 2416 return err 2417 2417 } 2418 2418 2419 - if len(t.Manifest) > 8192 { 2419 + if len(t.Manifest) > 1000000 { 2420 2420 return xerrors.Errorf("Value in field t.Manifest was too long") 2421 2421 } 2422 2422 ··· 2428 2428 } 2429 2429 2430 2430 // t.SbomBlob (util.LexBlob) (struct) 2431 - if len("sbomBlob") > 8192 { 2431 + if len("sbomBlob") > 1000000 { 2432 2432 return xerrors.Errorf("Value in field \"sbomBlob\" was too long") 2433 2433 } 2434 2434 ··· 2444 2444 } 2445 2445 2446 2446 // t.ScannedAt (string) (string) 2447 - if len("scannedAt") > 8192 { 2447 + if len("scannedAt") > 1000000 { 2448 2448 return xerrors.Errorf("Value in field \"scannedAt\" was too long") 2449 2449 } 2450 2450 ··· 2455 2455 return err 2456 2456 } 2457 2457 2458 - if len(t.ScannedAt) > 8192 { 2458 + if len(t.ScannedAt) > 1000000 { 2459 2459 return xerrors.Errorf("Value in field t.ScannedAt was too long") 2460 2460 } 2461 2461 ··· 2467 2467 } 2468 2468 2469 2469 // t.Repository (string) (string) 2470 - if len("repository") > 8192 { 2470 + if len("repository") > 1000000 { 2471 2471 return xerrors.Errorf("Value in field \"repository\" was too long") 2472 2472 } 2473 2473 ··· 2478 2478 return err 2479 2479 } 2480 2480 2481 - if len(t.Repository) > 8192 { 2481 + if len(t.Repository) > 1000000 { 2482 2482 return xerrors.Errorf("Value in field t.Repository was too long") 2483 2483 } 2484 2484 ··· 2490 2490 } 2491 2491 2492 2492 // t.ScannerVersion (string) (string) 2493 - if len("scannerVersion") > 8192 { 2493 + if len("scannerVersion") > 1000000 { 2494 2494 return xerrors.Errorf("Value in field \"scannerVersion\" was too long") 2495 2495 } 2496 2496 ··· 2501 2501 return err 2502 2502 } 2503 2503 2504 - if len(t.ScannerVersion) > 8192 { 2504 + if len(t.ScannerVersion) > 1000000 { 2505 2505 return xerrors.Errorf("Value in field t.ScannerVersion was too long") 2506 2506 } 2507 2507 ··· 2513 2513 } 2514 2514 2515 2515 // t.VulnReportBlob (util.LexBlob) (struct) 2516 - if len("vulnReportBlob") > 8192 { 2516 + if len("vulnReportBlob") > 1000000 { 2517 2517 return xerrors.Errorf("Value in field \"vulnReportBlob\" was too long") 2518 2518 } 2519 2519 ··· 2557 2557 2558 2558 nameBuf := make([]byte, 14) 2559 2559 for i := uint64(0); i < n; i++ { 2560 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 2560 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2561 2561 if err != nil { 2562 2562 return err 2563 2563 } ··· 2627 2627 case "$type": 2628 2628 2629 2629 { 2630 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2630 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2631 2631 if err != nil { 2632 2632 return err 2633 2633 } ··· 2690 2690 case "reason": 2691 2691 2692 2692 { 2693 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2693 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2694 2694 if err != nil { 2695 2695 return err 2696 2696 } ··· 2701 2701 case "status": 2702 2702 2703 2703 { 2704 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2704 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2705 2705 if err != nil { 2706 2706 return err 2707 2707 } ··· 2712 2712 case "userDid": 2713 2713 2714 2714 { 2715 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2715 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2716 2716 if err != nil { 2717 2717 return err 2718 2718 } ··· 2749 2749 case "manifest": 2750 2750 2751 2751 { 2752 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2752 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2753 2753 if err != nil { 2754 2754 return err 2755 2755 } ··· 2780 2780 case "scannedAt": 2781 2781 2782 2782 { 2783 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2783 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2784 2784 if err != nil { 2785 2785 return err 2786 2786 } ··· 2791 2791 case "repository": 2792 2792 2793 2793 { 2794 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2794 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2795 2795 if err != nil { 2796 2796 return err 2797 2797 } ··· 2802 2802 case "scannerVersion": 2803 2803 2804 2804 { 2805 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2805 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2806 2806 if err != nil { 2807 2807 return err 2808 2808 } ··· 2853 2853 } 2854 2854 2855 2855 // t.Type (string) (string) 2856 - if len("$type") > 8192 { 2856 + if len("$type") > 1000000 { 2857 2857 return xerrors.Errorf("Value in field \"$type\" was too long") 2858 2858 } 2859 2859 ··· 2864 2864 return err 2865 2865 } 2866 2866 2867 - if len(t.Type) > 8192 { 2867 + if len(t.Type) > 1000000 { 2868 2868 return xerrors.Errorf("Value in field t.Type was too long") 2869 2869 } 2870 2870 ··· 2876 2876 } 2877 2877 2878 2878 // t.Manifest (string) (string) 2879 - if len("manifest") > 8192 { 2879 + if len("manifest") > 1000000 { 2880 2880 return xerrors.Errorf("Value in field \"manifest\" was too long") 2881 2881 } 2882 2882 ··· 2887 2887 return err 2888 2888 } 2889 2889 2890 - if len(t.Manifest) > 8192 { 2890 + if len(t.Manifest) > 1000000 { 2891 2891 return xerrors.Errorf("Value in field t.Manifest was too long") 2892 2892 } 2893 2893 ··· 2899 2899 } 2900 2900 2901 2901 // t.CreatedAt (string) (string) 2902 - if len("createdAt") > 8192 { 2902 + if len("createdAt") > 1000000 { 2903 2903 return xerrors.Errorf("Value in field \"createdAt\" was too long") 2904 2904 } 2905 2905 ··· 2910 2910 return err 2911 2911 } 2912 2912 2913 - if len(t.CreatedAt) > 8192 { 2913 + if len(t.CreatedAt) > 1000000 { 2914 2914 return xerrors.Errorf("Value in field t.CreatedAt was too long") 2915 2915 } 2916 2916 ··· 2922 2922 } 2923 2923 2924 2924 // t.ConfigJSON (string) (string) 2925 - if len("configJson") > 8192 { 2925 + if len("configJson") > 1000000 { 2926 2926 return xerrors.Errorf("Value in field \"configJson\" was too long") 2927 2927 } 2928 2928 ··· 2933 2933 return err 2934 2934 } 2935 2935 2936 - if len(t.ConfigJSON) > 8192 { 2936 + if len(t.ConfigJSON) > 1000000 { 2937 2937 return xerrors.Errorf("Value in field t.ConfigJSON was too long") 2938 2938 } 2939 2939 ··· 2973 2973 2974 2974 nameBuf := make([]byte, 10) 2975 2975 for i := uint64(0); i < n; i++ { 2976 - nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192) 2976 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2977 2977 if err != nil { 2978 2978 return err 2979 2979 } ··· 2991 2991 case "$type": 2992 2992 2993 2993 { 2994 - sval, err := cbg.ReadStringWithMax(cr, 8192) 2994 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2995 2995 if err != nil { 2996 2996 return err 2997 2997 } ··· 3002 3002 case "manifest": 3003 3003 3004 3004 { 3005 - sval, err := cbg.ReadStringWithMax(cr, 8192) 3005 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3006 3006 if err != nil { 3007 3007 return err 3008 3008 } ··· 3013 3013 case "createdAt": 3014 3014 3015 3015 { 3016 - sval, err := cbg.ReadStringWithMax(cr, 8192) 3016 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3017 3017 if err != nil { 3018 3018 return err 3019 3019 } ··· 3024 3024 case "configJson": 3025 3025 3026 3026 { 3027 - sval, err := cbg.ReadStringWithMax(cr, 8192) 3027 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 3028 3028 if err != nil { 3029 3029 return err 3030 3030 }
+9 -2
pkg/atproto/generate.go
··· 25 25 ) 26 26 27 27 func main() { 28 - // Generate map-style encoders 29 - if err := cbg.WriteMapEncodersToFile("cbor_gen.go", "atproto", 28 + // MaxStringLength bumps the package-wide string read limit. cbor-gen v0.3.1 29 + // honors per-field `cborgen:"...,maxlen=N"` tags only on the write side; the 30 + // unmarshal template hard-codes the package default. ImageConfigRecord's 31 + // configJson holds raw OCI image configs that routinely exceed the stock 32 + // 8KB default (deep build histories, verbose created_by lines), so we lift 33 + // the ceiling for everyone. Per-field write caps still apply. 34 + gen := cbg.Gen{MaxStringLength: 1_000_000} 35 + 36 + if err := gen.WriteMapEncodersToFile("cbor_gen.go", "atproto", 30 37 atproto.CrewRecord{}, 31 38 atproto.CaptainRecord{}, 32 39 atproto.LayerRecord{},
+3 -3
pkg/atproto/lexicon.go
··· 932 932 // Stores the full OCI config JSON so the appview can display layer history including empty layers 933 933 type ImageConfigRecord struct { 934 934 Type string `json:"$type" cborgen:"$type"` 935 - Manifest string `json:"manifest" cborgen:"manifest"` // AT-URI of the manifest 936 - ConfigJSON string `json:"configJson" cborgen:"configJson"` // Raw OCI image config JSON 937 - CreatedAt string `json:"createdAt" cborgen:"createdAt"` // RFC3339 timestamp 935 + Manifest string `json:"manifest" cborgen:"manifest"` // AT-URI of the manifest 936 + ConfigJSON string `json:"configJson" cborgen:"configJson,maxlen=1000000"` // Raw OCI image config JSON. Cap mirrors lexicon maxLength so deep image histories (Bazel, multi-stage builds with verbose created_by lines) fit. 937 + CreatedAt string `json:"createdAt" cborgen:"createdAt"` // RFC3339 timestamp 938 938 } 939 939 940 940 // NewImageConfigRecord creates a new image config record
+70
pkg/atproto/lexicon_test.go
··· 1 1 package atproto 2 2 3 3 import ( 4 + "bytes" 4 5 "encoding/json" 5 6 "strings" 6 7 "testing" ··· 1385 1386 t.Errorf("Avatar.Ref.Link = %v, want %v", decoded.Avatar.Ref.Link, record.Avatar.Ref.Link) 1386 1387 } 1387 1388 } 1389 + 1390 + // TestImageConfigRecord_CBORRoundTrip locks in that we can encode and decode 1391 + // large OCI image configs without hitting cbor-gen's default 8KB string cap. 1392 + // Real-world images with deep build histories (Bazel, multi-stage Dockerfiles) 1393 + // routinely produce config blobs that blow past the default; if either side 1394 + // regresses, the backfill silently drops records with "configJson was too 1395 + // long" instead of populating the layer-history UI. 1396 + func TestImageConfigRecord_CBORRoundTrip(t *testing.T) { 1397 + tests := []struct { 1398 + name string 1399 + payloadSize int 1400 + }{ 1401 + // Small payloads exercise the happy path — should always have worked. 1402 + {"small", 1024}, 1403 + // 16KB is well past the old 8192 cborgen default. Pre-fix this would 1404 + // have failed at marshal time. 1405 + {"medium-16kb", 16 * 1024}, 1406 + // 200KB matches the upper end of pathological real configs (deep 1407 + // Bazel histories with verbose created_by lines). 1408 + {"large-200kb", 200 * 1024}, 1409 + // 900KB is just under our 1MB cap — the read side previously 1410 + // hard-coded 8192 even with the per-field write tag, so this also 1411 + // guards against the unmarshal regression. 1412 + {"near-cap-900kb", 900 * 1024}, 1413 + } 1414 + 1415 + for _, tc := range tests { 1416 + t.Run(tc.name, func(t *testing.T) { 1417 + // Use a repeating non-trivial pattern so any byte-level corruption 1418 + // in encode/decode shows up as a mismatch rather than blending 1419 + // into a sea of identical bytes. 1420 + const chunk = "history entry: bazel build //pkg:foo # " 1421 + payload := strings.Repeat(chunk, tc.payloadSize/len(chunk)+1)[:tc.payloadSize] 1422 + 1423 + record := &ImageConfigRecord{ 1424 + Type: ImageConfigCollection, 1425 + Manifest: "at://did:plc:test/io.atcr.manifest/abc", 1426 + ConfigJSON: payload, 1427 + CreatedAt: time.Now().UTC().Format(time.RFC3339), 1428 + } 1429 + 1430 + var buf bytes.Buffer 1431 + if err := record.MarshalCBOR(&buf); err != nil { 1432 + t.Fatalf("MarshalCBOR(%d bytes): %v", tc.payloadSize, err) 1433 + } 1434 + 1435 + var decoded ImageConfigRecord 1436 + if err := decoded.UnmarshalCBOR(&buf); err != nil { 1437 + t.Fatalf("UnmarshalCBOR(%d bytes): %v", tc.payloadSize, err) 1438 + } 1439 + 1440 + if decoded.Type != record.Type { 1441 + t.Errorf("Type = %q, want %q", decoded.Type, record.Type) 1442 + } 1443 + if decoded.Manifest != record.Manifest { 1444 + t.Errorf("Manifest = %q, want %q", decoded.Manifest, record.Manifest) 1445 + } 1446 + if decoded.CreatedAt != record.CreatedAt { 1447 + t.Errorf("CreatedAt = %q, want %q", decoded.CreatedAt, record.CreatedAt) 1448 + } 1449 + if len(decoded.ConfigJSON) != len(record.ConfigJSON) { 1450 + t.Fatalf("ConfigJSON length = %d, want %d", len(decoded.ConfigJSON), len(record.ConfigJSON)) 1451 + } 1452 + if decoded.ConfigJSON != record.ConfigJSON { 1453 + t.Errorf("ConfigJSON content mismatch at length %d", len(record.ConfigJSON)) 1454 + } 1455 + }) 1456 + } 1457 + }
+21
pkg/hold/admin/admin.go
··· 90 90 // In-memory session storage (single user, no persistence needed) 91 91 sessions map[string]*AdminSession 92 92 sessionsMu sync.RWMutex 93 + 94 + // scan-backfill state — runs as a background goroutine on click; the 95 + // status endpoint reads this for progress polling. Only one run at a 96 + // time (idempotent, so re-running is safe but pointless). 97 + scanBackfill scanBackfillState 98 + } 99 + 100 + // scanBackfillState tracks the in-flight scan-status backfill run. 101 + type scanBackfillState struct { 102 + mu sync.Mutex 103 + running bool 104 + startedAt time.Time 105 + current *pds.ScanBackfillResult // running totals (snapshot) 106 + result *pds.ScanBackfillResult // final result, set when running=false 107 + err string // last error (running ends with err set) 93 108 } 94 109 95 110 // adminContextKey is used to store session data in request context ··· 531 546 r.Get("/admin/api/top-users", ui.handleTopUsersAPI) 532 547 r.Get("/admin/api/relay/status", ui.handleRelayStatus) 533 548 r.Get("/admin/api/crew/member", ui.handleCrewMemberInfo) 549 + 550 + // Scan-record backfill: kicks off a background run and returns a 551 + // progress fragment that polls /status. Use Accept:application/json 552 + // for a synchronous JSON response (curl-friendly). 553 + r.Post("/admin/api/scan-backfill", ui.handleScanBackfill) 554 + r.Get("/admin/api/scan-backfill/status", ui.handleScanBackfillStatus) 534 555 535 556 // Logout 536 557 r.Post("/admin/auth/logout", ui.handleLogout)
+173
pkg/hold/admin/handlers_scan.go
··· 1 + package admin 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "log/slog" 8 + "net/http" 9 + "strings" 10 + "time" 11 + 12 + "atcr.io/pkg/hold/pds" 13 + ) 14 + 15 + // handleScanBackfill kicks off a scan-status backfill in a background 16 + // goroutine and returns a progress fragment that polls 17 + // /admin/api/scan-backfill/status for updates. Idempotent — clicking again 18 + // while a run is in flight just shows the current progress. 19 + // 20 + // Why background: reverse proxies typically cap upstream HTTP timeouts at 21 + // 10–60s, which would cancel a synchronous request mid-loop. Detaching the 22 + // work from the request context lets it run to completion. 23 + // 24 + // JSON callers (Accept: application/json) get a synchronous run instead — 25 + // useful for curl + scripting. 26 + func (ui *AdminUI) handleScanBackfill(w http.ResponseWriter, r *http.Request) { 27 + session := getSessionFromContext(r.Context()) 28 + wantJSON := strings.Contains(r.Header.Get("Accept"), "application/json") 29 + 30 + if wantJSON { 31 + // Synchronous JSON path — caller decides their own timeout. 32 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 33 + defer cancel() 34 + res, err := ui.pds.BackfillScanStatus(ctx, scanBackfillLogger, nil) 35 + if err != nil { 36 + slog.Error("scan-backfill failed", "by", session.DID, "error", err) 37 + http.Error(w, err.Error(), http.StatusInternalServerError) 38 + return 39 + } 40 + slog.Info("scan-status backfill complete (sync)", 41 + "by", session.DID, 42 + "scanned", res.Scanned, 43 + "already_tagged", res.AlreadyTagged, 44 + "marked_skipped", res.MarkedSkipped, 45 + "marked_failed", res.MarkedFailed, 46 + "rewritten", res.Rewritten, 47 + ) 48 + w.Header().Set("Content-Type", "application/json") 49 + _ = json.NewEncoder(w).Encode(res) 50 + return 51 + } 52 + 53 + // HTML path — kick off the background run if one isn't already going. 54 + st := &ui.scanBackfill 55 + st.mu.Lock() 56 + alreadyRunning := st.running 57 + if !alreadyRunning { 58 + st.running = true 59 + st.startedAt = time.Now() 60 + st.current = &pds.ScanBackfillResult{} 61 + st.result = nil 62 + st.err = "" 63 + } 64 + st.mu.Unlock() 65 + 66 + if !alreadyRunning { 67 + slog.Info("scan-status backfill started via admin panel", "by", session.DID) 68 + go ui.runScanBackfill() 69 + } else { 70 + slog.Debug("scan-status backfill already in progress; returning current state") 71 + } 72 + 73 + ui.renderTemplate(w, "partials/scan_backfill_progress.html", ui.snapshotScanBackfill()) 74 + } 75 + 76 + // handleScanBackfillStatus is polled by the progress fragment. Returns the 77 + // progress fragment again if running, the result fragment when done, or an 78 + // error fragment if something went wrong. 79 + func (ui *AdminUI) handleScanBackfillStatus(w http.ResponseWriter, r *http.Request) { 80 + snap := ui.snapshotScanBackfill() 81 + if snap.Running { 82 + ui.renderTemplate(w, "partials/scan_backfill_progress.html", snap) 83 + return 84 + } 85 + if snap.Error != "" { 86 + ui.renderTemplate(w, "partials/gc_error.html", struct{ Error string }{snap.Error}) 87 + return 88 + } 89 + if snap.Result == nil { 90 + // Initial state, before any run — render an empty placeholder. 91 + _, _ = w.Write([]byte("")) 92 + return 93 + } 94 + ui.renderTemplate(w, "partials/scan_backfill_result.html", snap.Result) 95 + } 96 + 97 + // runScanBackfill is the goroutine body. Updates the shared state as the 98 + // backfill progresses and stores the final result (or error) when it ends. 99 + func (ui *AdminUI) runScanBackfill() { 100 + st := &ui.scanBackfill 101 + defer func() { 102 + st.mu.Lock() 103 + st.running = false 104 + st.mu.Unlock() 105 + }() 106 + 107 + // Generous independent timeout — the loop is single-threaded and large 108 + // holds with thousands of legacy records can take a few minutes. 109 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) 110 + defer cancel() 111 + 112 + res, err := ui.pds.BackfillScanStatus(ctx, scanBackfillLogger, func(snap *pds.ScanBackfillResult) { 113 + // Copy so we don't keep a pointer the loop will mutate. 114 + c := *snap 115 + st.mu.Lock() 116 + st.current = &c 117 + st.mu.Unlock() 118 + }) 119 + st.mu.Lock() 120 + if err != nil { 121 + st.err = err.Error() 122 + slog.Error("scan-status backfill failed", "error", err) 123 + } else { 124 + st.result = res 125 + slog.Info("scan-status backfill complete", 126 + "scanned", res.Scanned, 127 + "already_tagged", res.AlreadyTagged, 128 + "marked_skipped", res.MarkedSkipped, 129 + "marked_failed", res.MarkedFailed, 130 + "rewritten", res.Rewritten, 131 + ) 132 + } 133 + st.mu.Unlock() 134 + } 135 + 136 + // scanBackfillSnapshot is the shape exposed to templates. 137 + type scanBackfillSnapshot struct { 138 + Running bool 139 + StartedAt time.Time 140 + Current *pds.ScanBackfillResult // populated while running 141 + Result *pds.ScanBackfillResult // populated when complete 142 + Error string 143 + } 144 + 145 + // snapshotScanBackfill returns a copy of the current state — safe to render 146 + // without holding the mutex. 147 + func (ui *AdminUI) snapshotScanBackfill() scanBackfillSnapshot { 148 + st := &ui.scanBackfill 149 + st.mu.Lock() 150 + defer st.mu.Unlock() 151 + 152 + snap := scanBackfillSnapshot{ 153 + Running: st.running, 154 + StartedAt: st.startedAt, 155 + Error: st.err, 156 + } 157 + if st.current != nil { 158 + c := *st.current 159 + snap.Current = &c 160 + } 161 + if st.result != nil { 162 + r := *st.result 163 + snap.Result = &r 164 + } 165 + return snap 166 + } 167 + 168 + // scanBackfillLogger formats the printf-style messages from BackfillScanStatus 169 + // into a single slog message — slog's variadic args are key/value pairs, not 170 + // printf operands. 171 + func scanBackfillLogger(format string, args ...any) { 172 + slog.Warn("scan-backfill: " + fmt.Sprintf(format, args...)) 173 + }
+20
pkg/hold/admin/templates/partials/scan_backfill_progress.html
··· 1 + {{define "partials/scan_backfill_progress.html"}} 2 + <div hx-get="/admin/api/scan-backfill/status" 3 + hx-trigger="load delay:1s" 4 + hx-swap="outerHTML" 5 + class="alert alert-info"> 6 + <span class="loading loading-spinner loading-sm" aria-hidden="true"></span> 7 + <div class="text-sm"> 8 + <p class="font-medium">Backfilling scan records...</p> 9 + {{ if .Current }} 10 + <p class="text-base-content/70 mt-1"> 11 + Scanned <span class="font-mono">{{ .Current.Scanned }}</span> records · 12 + rewrites: <span class="font-mono">{{ .Current.Rewritten }}</span> 13 + ({{ .Current.MarkedSkipped }} skipped, {{ .Current.MarkedFailed }} failed) 14 + </p> 15 + {{ else }} 16 + <p class="text-base-content/70 mt-1">Starting...</p> 17 + {{ end }} 18 + </div> 19 + </div> 20 + {{end}}
+15
pkg/hold/admin/templates/partials/scan_backfill_result.html
··· 1 + {{define "partials/scan_backfill_result.html"}} 2 + <div class="alert alert-success"> 3 + {{ icon "check-circle" "size-4 shrink-0" }} 4 + <div> 5 + <p class="font-medium">Scan-record backfill complete</p> 6 + <ul class="text-sm mt-1 space-y-0.5"> 7 + <li>Scanned: <span class="font-mono">{{ .Scanned }}</span></li> 8 + <li>Already tagged: <span class="font-mono">{{ .AlreadyTagged }}</span></li> 9 + <li>→ Skipped (helm / in-toto / DSSE): <span class="font-mono">{{ .MarkedSkipped }}</span></li> 10 + <li>→ Failed (legacy errors): <span class="font-mono">{{ .MarkedFailed }}</span></li> 11 + <li>Total rewritten: <span class="font-mono">{{ .Rewritten }}</span></li> 12 + </ul> 13 + </div> 14 + </div> 15 + {{end}}
+27
pkg/hold/admin/templates/partials/tab_storage.html
··· 67 67 </div> 68 68 {{end}} 69 69 </div> 70 + 71 + <!-- Scan-record maintenance --> 72 + <div class="card bg-base-100 shadow-sm mb-6"> 73 + <div class="card-body"> 74 + <h2 class="card-title text-lg">Scan records</h2> 75 + <p class="text-sm text-base-content/70"> 76 + Rewrite legacy scan records (created before the <code class="text-xs">status</code> field 77 + existed) so the appview can distinguish intentionally skipped artifacts (helm charts, 78 + in-toto, DSSE) from genuine failures. Idempotent — safe to re-run. 79 + </p> 80 + <div class="flex gap-3 mt-2"> 81 + <button class="btn btn-outline gap-2" 82 + hx-post="/admin/api/scan-backfill" 83 + hx-target="#scan-backfill-result" 84 + hx-swap="innerHTML" 85 + hx-disabled-elt="this" 86 + hx-indicator="#scan-backfill-spinner"> 87 + {{ icon "refresh-cw" "size-4" }} 88 + Backfill scan statuses 89 + </button> 90 + <span id="scan-backfill-spinner" class="htmx-indicator self-center" aria-hidden="true"> 91 + <span class="loading loading-spinner loading-sm"></span> 92 + </span> 93 + </div> 94 + <div id="scan-backfill-result" class="mt-3" aria-live="polite"></div> 95 + </div> 96 + </div> 70 97 {{end}}
+151
pkg/hold/pds/scan.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 + "strings" 6 7 7 8 "atcr.io/pkg/atproto" 8 9 "github.com/ipfs/go-cid" 9 10 ) 11 + 12 + // unscannableLayerMediaSubstrings lists layer media-type fragments that 13 + // identify artifact types the scanner intentionally skips. Kept in sync with 14 + // scanner/internal/scan/worker.go's unscannableConfigTypes — that map keys on 15 + // config media types; here we look at *layer* media types because the 16 + // backfill walks the hold's layer index (manifest AT-URIs join layers to 17 + // scan records). 18 + var unscannableLayerMediaSubstrings = []string{ 19 + "helm.chart.content", 20 + "in-toto", 21 + "dsse.envelope", 22 + } 23 + 24 + // ScanBackfillResult summarizes what BackfillScanStatus did. 25 + type ScanBackfillResult struct { 26 + Scanned int // total scan records examined 27 + AlreadyTagged int // records with non-empty status (or non-failure shape) — left alone 28 + MarkedSkipped int // records rewritten as status=skipped 29 + MarkedFailed int // records rewritten as status=failed 30 + Rewritten int // total rewrites (MarkedSkipped + MarkedFailed) that succeeded 31 + } 32 + 33 + // BackfillScanStatus walks every io.atcr.hold.scan record on this hold and 34 + // assigns a status ("skipped" or "failed") to legacy records that pre-date the 35 + // status field. Idempotent — records that already have a non-empty status are 36 + // left alone. 37 + // 38 + // A legacy record is one with empty status, no SBOM blob, and zero 39 + // vulnerability counts. Layer media types are inspected to choose the 40 + // rewrite: helm/in-toto/DSSE → skipped, otherwise → failed. ScannedAt is 41 + // preserved on rewrite so the rescan timer doesn't reset. 42 + // 43 + // Safe to call on a running hold — uses the existing repomgr APIs the same 44 + // way scan-job handling does. 45 + // 46 + // progress, if non-nil, is called periodically with a snapshot of the 47 + // running totals so callers can surface progress to the UI. The callback 48 + // must not retain the pointer past its return. 49 + func (p *HoldPDS) BackfillScanStatus(ctx context.Context, log func(format string, args ...any), progress func(*ScanBackfillResult)) (*ScanBackfillResult, error) { 50 + if log == nil { 51 + log = func(string, ...any) {} 52 + } 53 + if progress == nil { 54 + progress = func(*ScanBackfillResult) {} 55 + } 56 + ri := p.RecordsIndex() 57 + if ri == nil { 58 + return nil, fmt.Errorf("records index not available") 59 + } 60 + 61 + const batchSize = 200 62 + res := &ScanBackfillResult{} 63 + var cursor string 64 + 65 + for { 66 + records, nextCursor, err := ri.ListRecords(atproto.ScanCollection, batchSize, cursor, true) 67 + if err != nil { 68 + return res, fmt.Errorf("list scan records: %w", err) 69 + } 70 + 71 + for _, rec := range records { 72 + if err := ctx.Err(); err != nil { 73 + return res, err 74 + } 75 + res.Scanned++ 76 + manifestDigest := "sha256:" + rec.Rkey 77 + 78 + _, scanRecord, err := p.GetScanRecord(ctx, manifestDigest) 79 + if err != nil { 80 + log("skip rkey=%s: get failed: %v", rec.Rkey, err) 81 + continue 82 + } 83 + 84 + // Already classified — nothing to do. 85 + if scanRecord.Status != "" { 86 + res.AlreadyTagged++ 87 + continue 88 + } 89 + // Real data present (legacy successful scan) — treat as ok, don't rewrite. 90 + if scanRecord.SbomBlob != nil || scanRecord.Total != 0 { 91 + res.AlreadyTagged++ 92 + continue 93 + } 94 + 95 + // Determine artifact type from layer media types. 96 + layers, err := p.ListLayerRecordsForManifest(ctx, scanRecord.Manifest) 97 + if err != nil { 98 + log("skip rkey=%s: list layers failed: %v", rec.Rkey, err) 99 + continue 100 + } 101 + 102 + skipped := false 103 + for _, l := range layers { 104 + for _, frag := range unscannableLayerMediaSubstrings { 105 + if strings.Contains(l.MediaType, frag) { 106 + skipped = true 107 + break 108 + } 109 + } 110 + if skipped { 111 + break 112 + } 113 + } 114 + 115 + var rewrite *atproto.ScanRecord 116 + if skipped { 117 + rewrite = atproto.NewSkippedScanRecord( 118 + manifestDigest, 119 + scanRecord.Repository, 120 + scanRecord.UserDID, 121 + "backfilled: unscannable artifact type", 122 + scanRecord.ScannerVersion, 123 + ) 124 + res.MarkedSkipped++ 125 + } else { 126 + rewrite = atproto.NewFailedScanRecord( 127 + manifestDigest, 128 + scanRecord.Repository, 129 + scanRecord.UserDID, 130 + "backfilled: legacy record (no SBOM and zero counts)", 131 + scanRecord.ScannerVersion, 132 + ) 133 + res.MarkedFailed++ 134 + } 135 + // Preserve original ScannedAt so the rescan timer doesn't reset 136 + // and we don't lose audit signal. 137 + if scanRecord.ScannedAt != "" { 138 + rewrite.ScannedAt = scanRecord.ScannedAt 139 + } 140 + 141 + if _, _, err := p.CreateScanRecord(ctx, rewrite); err != nil { 142 + log("rewrite rkey=%s failed: %v", rec.Rkey, err) 143 + continue 144 + } 145 + res.Rewritten++ 146 + } 147 + 148 + // Report progress at every batch boundary — gives the UI smooth-ish 149 + // updates without locking the loop on every record. 150 + progress(res) 151 + 152 + if nextCursor == "" || len(records) == 0 { 153 + break 154 + } 155 + cursor = nextCursor 156 + } 157 + 158 + progress(res) 159 + return res, nil 160 + } 10 161 11 162 // CreateScanRecord creates or updates a scan result record in the hold's PDS 12 163 // Uses a deterministic rkey based on the manifest digest, so re-scans upsert