mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-20 09:14:16 +00:00
add scan reports to hold pds
This commit is contained in:
+582
-1
@@ -8,6 +8,7 @@ import (
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
util "github.com/bluesky-social/indigo/lex/util"
|
||||
cid "github.com/ipfs/go-cid"
|
||||
cbg "github.com/whyrusleeping/cbor-gen"
|
||||
xerrors "golang.org/x/xerrors"
|
||||
@@ -25,7 +26,7 @@ func (t *CrewRecord) MarshalCBOR(w io.Writer) error {
|
||||
}
|
||||
|
||||
cw := cbg.NewCborWriter(w)
|
||||
fieldCount := 6
|
||||
fieldCount := 7
|
||||
|
||||
if t.Tier == "" {
|
||||
fieldCount--
|
||||
@@ -153,6 +154,22 @@ func (t *CrewRecord) MarshalCBOR(w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.Plankowner (bool) (bool)
|
||||
if len("plankowner") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"plankowner\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("plankowner"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("plankowner")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cbg.WriteBool(w, t.Plankowner); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.Permissions ([]string) (slice)
|
||||
if len("permissions") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"permissions\" was too long")
|
||||
@@ -284,6 +301,24 @@ func (t *CrewRecord) UnmarshalCBOR(r io.Reader) (err error) {
|
||||
|
||||
t.AddedAt = string(sval)
|
||||
}
|
||||
// t.Plankowner (bool) (bool)
|
||||
case "plankowner":
|
||||
|
||||
maj, extra, err = cr.ReadHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if maj != cbg.MajOther {
|
||||
return fmt.Errorf("booleans must be major type 7")
|
||||
}
|
||||
switch extra {
|
||||
case 20:
|
||||
t.Plankowner = false
|
||||
case 21:
|
||||
t.Plankowner = true
|
||||
default:
|
||||
return fmt.Errorf("booleans are either major type 7, value 20 or 21 (got %d)", extra)
|
||||
}
|
||||
// t.Permissions ([]string) (slice)
|
||||
case "permissions":
|
||||
|
||||
@@ -1767,3 +1802,549 @@ func (t *StatsRecord) UnmarshalCBOR(r io.Reader) (err error) {
|
||||
|
||||
return nil
|
||||
}
|
||||
func (t *ScanRecord) MarshalCBOR(w io.Writer) error {
|
||||
if t == nil {
|
||||
_, err := w.Write(cbg.CborNull)
|
||||
return err
|
||||
}
|
||||
|
||||
cw := cbg.NewCborWriter(w)
|
||||
|
||||
if _, err := cw.Write([]byte{172}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.Low (int64) (int64)
|
||||
if len("low") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"low\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("low"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("low")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.Low >= 0 {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Low)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Low-1)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// t.High (int64) (int64)
|
||||
if len("high") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"high\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("high"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("high")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.High >= 0 {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.High)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.High-1)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// t.Type (string) (string)
|
||||
if len("$type") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"$type\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("$type")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(t.Type) > 8192 {
|
||||
return xerrors.Errorf("Value in field t.Type was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Type))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string(t.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.Total (int64) (int64)
|
||||
if len("total") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"total\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("total"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("total")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.Total >= 0 {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Total)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Total-1)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// t.Medium (int64) (int64)
|
||||
if len("medium") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"medium\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("medium"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("medium")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.Medium >= 0 {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Medium)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Medium-1)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// t.UserDID (string) (string)
|
||||
if len("userDid") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"userDid\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("userDid"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("userDid")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(t.UserDID) > 8192 {
|
||||
return xerrors.Errorf("Value in field t.UserDID was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.UserDID))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string(t.UserDID)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.Critical (int64) (int64)
|
||||
if len("critical") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"critical\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("critical"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("critical")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if t.Critical >= 0 {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Critical)); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajNegativeInt, uint64(-t.Critical-1)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// t.Manifest (string) (string)
|
||||
if len("manifest") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"manifest\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("manifest"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("manifest")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(t.Manifest) > 8192 {
|
||||
return xerrors.Errorf("Value in field t.Manifest was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Manifest))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string(t.Manifest)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.SbomBlob (util.LexBlob) (struct)
|
||||
if len("sbomBlob") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"sbomBlob\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sbomBlob"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("sbomBlob")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := t.SbomBlob.MarshalCBOR(cw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.ScannedAt (string) (string)
|
||||
if len("scannedAt") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"scannedAt\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("scannedAt"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("scannedAt")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(t.ScannedAt) > 8192 {
|
||||
return xerrors.Errorf("Value in field t.ScannedAt was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.ScannedAt))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string(t.ScannedAt)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.Repository (string) (string)
|
||||
if len("repository") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"repository\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repository"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("repository")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(t.Repository) > 8192 {
|
||||
return xerrors.Errorf("Value in field t.Repository was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Repository))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string(t.Repository)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// t.ScannerVersion (string) (string)
|
||||
if len("scannerVersion") > 8192 {
|
||||
return xerrors.Errorf("Value in field \"scannerVersion\" was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("scannerVersion"))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string("scannerVersion")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(t.ScannerVersion) > 8192 {
|
||||
return xerrors.Errorf("Value in field t.ScannerVersion was too long")
|
||||
}
|
||||
|
||||
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.ScannerVersion))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := cw.WriteString(string(t.ScannerVersion)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ScanRecord) UnmarshalCBOR(r io.Reader) (err error) {
|
||||
*t = ScanRecord{}
|
||||
|
||||
cr := cbg.NewCborReader(r)
|
||||
|
||||
maj, extra, err := cr.ReadHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
}()
|
||||
|
||||
if maj != cbg.MajMap {
|
||||
return fmt.Errorf("cbor input should be of type map")
|
||||
}
|
||||
|
||||
if extra > cbg.MaxLength {
|
||||
return fmt.Errorf("ScanRecord: map struct too large (%d)", extra)
|
||||
}
|
||||
|
||||
n := extra
|
||||
|
||||
nameBuf := make([]byte, 14)
|
||||
for i := uint64(0); i < n; i++ {
|
||||
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
// Field doesn't exist on this type, so ignore it
|
||||
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch string(nameBuf[:nameLen]) {
|
||||
// t.Low (int64) (int64)
|
||||
case "low":
|
||||
{
|
||||
maj, extra, err := cr.ReadHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var extraI int64
|
||||
switch maj {
|
||||
case cbg.MajUnsignedInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 positive overflow")
|
||||
}
|
||||
case cbg.MajNegativeInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 negative overflow")
|
||||
}
|
||||
extraI = -1 - extraI
|
||||
default:
|
||||
return fmt.Errorf("wrong type for int64 field: %d", maj)
|
||||
}
|
||||
|
||||
t.Low = int64(extraI)
|
||||
}
|
||||
// t.High (int64) (int64)
|
||||
case "high":
|
||||
{
|
||||
maj, extra, err := cr.ReadHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var extraI int64
|
||||
switch maj {
|
||||
case cbg.MajUnsignedInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 positive overflow")
|
||||
}
|
||||
case cbg.MajNegativeInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 negative overflow")
|
||||
}
|
||||
extraI = -1 - extraI
|
||||
default:
|
||||
return fmt.Errorf("wrong type for int64 field: %d", maj)
|
||||
}
|
||||
|
||||
t.High = int64(extraI)
|
||||
}
|
||||
// t.Type (string) (string)
|
||||
case "$type":
|
||||
|
||||
{
|
||||
sval, err := cbg.ReadStringWithMax(cr, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Type = string(sval)
|
||||
}
|
||||
// t.Total (int64) (int64)
|
||||
case "total":
|
||||
{
|
||||
maj, extra, err := cr.ReadHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var extraI int64
|
||||
switch maj {
|
||||
case cbg.MajUnsignedInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 positive overflow")
|
||||
}
|
||||
case cbg.MajNegativeInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 negative overflow")
|
||||
}
|
||||
extraI = -1 - extraI
|
||||
default:
|
||||
return fmt.Errorf("wrong type for int64 field: %d", maj)
|
||||
}
|
||||
|
||||
t.Total = int64(extraI)
|
||||
}
|
||||
// t.Medium (int64) (int64)
|
||||
case "medium":
|
||||
{
|
||||
maj, extra, err := cr.ReadHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var extraI int64
|
||||
switch maj {
|
||||
case cbg.MajUnsignedInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 positive overflow")
|
||||
}
|
||||
case cbg.MajNegativeInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 negative overflow")
|
||||
}
|
||||
extraI = -1 - extraI
|
||||
default:
|
||||
return fmt.Errorf("wrong type for int64 field: %d", maj)
|
||||
}
|
||||
|
||||
t.Medium = int64(extraI)
|
||||
}
|
||||
// t.UserDID (string) (string)
|
||||
case "userDid":
|
||||
|
||||
{
|
||||
sval, err := cbg.ReadStringWithMax(cr, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.UserDID = string(sval)
|
||||
}
|
||||
// t.Critical (int64) (int64)
|
||||
case "critical":
|
||||
{
|
||||
maj, extra, err := cr.ReadHeader()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var extraI int64
|
||||
switch maj {
|
||||
case cbg.MajUnsignedInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 positive overflow")
|
||||
}
|
||||
case cbg.MajNegativeInt:
|
||||
extraI = int64(extra)
|
||||
if extraI < 0 {
|
||||
return fmt.Errorf("int64 negative overflow")
|
||||
}
|
||||
extraI = -1 - extraI
|
||||
default:
|
||||
return fmt.Errorf("wrong type for int64 field: %d", maj)
|
||||
}
|
||||
|
||||
t.Critical = int64(extraI)
|
||||
}
|
||||
// t.Manifest (string) (string)
|
||||
case "manifest":
|
||||
|
||||
{
|
||||
sval, err := cbg.ReadStringWithMax(cr, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Manifest = string(sval)
|
||||
}
|
||||
// t.SbomBlob (util.LexBlob) (struct)
|
||||
case "sbomBlob":
|
||||
|
||||
{
|
||||
|
||||
b, err := cr.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if b != cbg.CborNull[0] {
|
||||
if err := cr.UnreadByte(); err != nil {
|
||||
return err
|
||||
}
|
||||
t.SbomBlob = new(util.LexBlob)
|
||||
if err := t.SbomBlob.UnmarshalCBOR(cr); err != nil {
|
||||
return xerrors.Errorf("unmarshaling t.SbomBlob pointer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// t.ScannedAt (string) (string)
|
||||
case "scannedAt":
|
||||
|
||||
{
|
||||
sval, err := cbg.ReadStringWithMax(cr, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.ScannedAt = string(sval)
|
||||
}
|
||||
// t.Repository (string) (string)
|
||||
case "repository":
|
||||
|
||||
{
|
||||
sval, err := cbg.ReadStringWithMax(cr, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Repository = string(sval)
|
||||
}
|
||||
// t.ScannerVersion (string) (string)
|
||||
case "scannerVersion":
|
||||
|
||||
{
|
||||
sval, err := cbg.ReadStringWithMax(cr, 8192)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.ScannerVersion = string(sval)
|
||||
}
|
||||
|
||||
default:
|
||||
// Field doesn't exist on this type, so ignore it
|
||||
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func main() {
|
||||
atproto.LayerRecord{},
|
||||
atproto.TangledProfileRecord{},
|
||||
atproto.StatsRecord{},
|
||||
atproto.ScanRecord{},
|
||||
); err != nil {
|
||||
fmt.Printf("Failed to generate CBOR encoders: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
||||
+56
-1
@@ -10,6 +10,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lexutil "github.com/bluesky-social/indigo/lex/util"
|
||||
)
|
||||
|
||||
// Collection names for ATProto records
|
||||
@@ -41,6 +43,10 @@ const (
|
||||
// Stored in hold's embedded PDS to track pull/push counts per owner+repo
|
||||
StatsCollection = "io.atcr.hold.stats"
|
||||
|
||||
// ScanCollection is the collection name for vulnerability scan results
|
||||
// Stored in hold's embedded PDS to track scan results per manifest
|
||||
ScanCollection = "io.atcr.hold.scan"
|
||||
|
||||
// TangledProfileCollection is the collection name for tangled profiles
|
||||
// Stored in hold's embedded PDS (singleton record at rkey "self")
|
||||
TangledProfileCollection = "sh.tangled.actor.profile"
|
||||
@@ -594,7 +600,7 @@ type CrewRecord struct {
|
||||
Role string `json:"role" cborgen:"role"`
|
||||
Permissions []string `json:"permissions" cborgen:"permissions"`
|
||||
Tier string `json:"tier,omitempty" cborgen:"tier,omitempty"` // Optional tier for quota limits (e.g., 'deckhand', 'bosun', 'quartermaster')
|
||||
Plankowner bool `json:"plankowner,omitempty" cborgen:"plankowner,omitempty"` // Early adopter flag - gets plankowner_crew_tier for free
|
||||
Plankowner bool `json:"plankowner,omitempty" cborgen:"plankowner"` // Early adopter flag - gets plankowner_crew_tier for free
|
||||
AddedAt string `json:"addedAt" cborgen:"addedAt"` // RFC3339 timestamp
|
||||
}
|
||||
|
||||
@@ -675,6 +681,55 @@ func CrewRecordKey(memberDID string) string {
|
||||
return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:16]))
|
||||
}
|
||||
|
||||
// ScanRecord represents vulnerability scan results for a manifest
|
||||
// Collection: io.atcr.hold.scan
|
||||
// Stored in hold's embedded PDS to track scan results per manifest
|
||||
// Uses CBOR encoding for efficient storage in hold's carstore
|
||||
// RKey is deterministic: based on manifest digest (one scan per manifest)
|
||||
type ScanRecord struct {
|
||||
Type string `json:"$type" cborgen:"$type"`
|
||||
Manifest string `json:"manifest" cborgen:"manifest"` // AT-URI of the scanned manifest (e.g., "at://did:plc:xyz/io.atcr.manifest/abc123...")
|
||||
Repository string `json:"repository" cborgen:"repository"` // Repository name (e.g., "myapp")
|
||||
UserDID string `json:"userDid" cborgen:"userDid"` // DID of the image owner
|
||||
SbomBlob *lexutil.LexBlob `json:"sbomBlob,omitempty" cborgen:"sbomBlob"` // SBOM blob uploaded to hold's PDS blob storage
|
||||
Critical int64 `json:"critical" cborgen:"critical"` // Count of critical vulnerabilities
|
||||
High int64 `json:"high" cborgen:"high"` // Count of high vulnerabilities
|
||||
Medium int64 `json:"medium" cborgen:"medium"` // Count of medium vulnerabilities
|
||||
Low int64 `json:"low" cborgen:"low"` // Count of low vulnerabilities
|
||||
Total int64 `json:"total" cborgen:"total"` // Total vulnerability count
|
||||
ScannerVersion string `json:"scannerVersion" cborgen:"scannerVersion"` // Scanner version (e.g., "atcr-scanner-v1.0.0")
|
||||
ScannedAt string `json:"scannedAt" cborgen:"scannedAt"` // RFC3339 timestamp of scan completion
|
||||
}
|
||||
|
||||
// NewScanRecord creates a new scan record
|
||||
// manifestDigest: the manifest digest (e.g., "sha256:abc123...")
|
||||
// userDID: the DID of the image owner (used to build the manifest AT-URI)
|
||||
// sbomBlob: blob reference from uploading SBOM to PDS blob storage (nil if no SBOM)
|
||||
func NewScanRecord(manifestDigest, repository, userDID string, sbomBlob *lexutil.LexBlob, critical, high, medium, low, total int, scannerVersion string) *ScanRecord {
|
||||
return &ScanRecord{
|
||||
Type: ScanCollection,
|
||||
Manifest: BuildManifestURI(userDID, manifestDigest),
|
||||
Repository: repository,
|
||||
UserDID: userDID,
|
||||
SbomBlob: sbomBlob,
|
||||
Critical: int64(critical),
|
||||
High: int64(high),
|
||||
Medium: int64(medium),
|
||||
Low: int64(low),
|
||||
Total: int64(total),
|
||||
ScannerVersion: scannerVersion,
|
||||
ScannedAt: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
// ScanRecordKey generates a deterministic record key for a scan result
|
||||
// Uses the manifest digest (without algorithm prefix) as the rkey
|
||||
// This ensures one scan record per manifest, and re-scans upsert the record
|
||||
func ScanRecordKey(manifestDigest string) string {
|
||||
// Remove the "sha256:" prefix - the hex digest is already a valid rkey
|
||||
return strings.TrimPrefix(manifestDigest, "sha256:")
|
||||
}
|
||||
|
||||
// TangledProfileRecord represents a Tangled profile for the hold
|
||||
// Collection: sh.tangled.actor.profile (singleton record at rkey "self")
|
||||
// Stored in the hold's embedded PDS
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package pds
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"atcr.io/pkg/atproto"
|
||||
"github.com/ipfs/go-cid"
|
||||
)
|
||||
|
||||
// CreateScanRecord creates or updates a scan result record in the hold's PDS
|
||||
// Uses a deterministic rkey based on the manifest digest, so re-scans upsert
|
||||
func (p *HoldPDS) CreateScanRecord(ctx context.Context, record *atproto.ScanRecord) (string, cid.Cid, error) {
|
||||
if record.Type != atproto.ScanCollection {
|
||||
return "", cid.Undef, fmt.Errorf("invalid record type: %s", record.Type)
|
||||
}
|
||||
|
||||
if record.Manifest == "" {
|
||||
return "", cid.Undef, fmt.Errorf("manifest AT-URI is required")
|
||||
}
|
||||
|
||||
// Extract the digest from the manifest AT-URI to use as rkey
|
||||
manifestDigest, err := atproto.ParseManifestURI(record.Manifest)
|
||||
if err != nil {
|
||||
return "", cid.Undef, fmt.Errorf("invalid manifest AT-URI: %w", err)
|
||||
}
|
||||
rkey := atproto.ScanRecordKey(manifestDigest)
|
||||
|
||||
// Upsert: re-scans update the existing record
|
||||
rpath, recordCID, _, err := p.repomgr.UpsertRecord(
|
||||
ctx,
|
||||
p.uid,
|
||||
atproto.ScanCollection,
|
||||
rkey,
|
||||
record,
|
||||
)
|
||||
if err != nil {
|
||||
return "", cid.Undef, fmt.Errorf("failed to upsert scan record: %w", err)
|
||||
}
|
||||
|
||||
return rpath, recordCID, nil
|
||||
}
|
||||
|
||||
// GetScanRecord retrieves a scan result record by manifest digest
|
||||
func (p *HoldPDS) GetScanRecord(ctx context.Context, manifestDigest string) (cid.Cid, *atproto.ScanRecord, error) {
|
||||
rkey := atproto.ScanRecordKey(manifestDigest)
|
||||
|
||||
recordCID, val, err := p.repomgr.GetRecord(ctx, p.uid, atproto.ScanCollection, rkey, cid.Undef)
|
||||
if err != nil {
|
||||
return cid.Undef, nil, fmt.Errorf("failed to get scan record: %w", err)
|
||||
}
|
||||
|
||||
scanRecord, ok := val.(*atproto.ScanRecord)
|
||||
if !ok {
|
||||
return cid.Undef, nil, fmt.Errorf("unexpected type for scan record: %T", val)
|
||||
}
|
||||
|
||||
return recordCID, scanRecord, nil
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package pds
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -12,6 +11,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"atcr.io/pkg/atproto"
|
||||
lexutil "github.com/bluesky-social/indigo/lex/util"
|
||||
storagedriver "github.com/distribution/distribution/v3/registry/storage/driver"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
@@ -353,7 +354,7 @@ func (sb *ScanBroadcaster) handleAck(sub *ScanSubscriber, seq int64) {
|
||||
"subscriberId", sub.id)
|
||||
}
|
||||
|
||||
// handleResult processes a completed scan result: stores ORAS manifest + marks completed
|
||||
// handleResult processes a completed scan result: uploads SBOM blob + stores scan record in PDS
|
||||
func (sb *ScanBroadcaster) handleResult(sub *ScanSubscriber, msg ScannerMessage) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -382,36 +383,40 @@ func (sb *ScanBroadcaster) handleResult(sub *ScanSubscriber, msg ScannerMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// Store vulnerability report blob in S3
|
||||
if msg.VulnReport != "" {
|
||||
vulnJSON := []byte(msg.VulnReport)
|
||||
vulnDigest := fmt.Sprintf("sha256:%x", sha256.Sum256(vulnJSON))
|
||||
|
||||
if err := sb.uploadBlob(ctx, vulnDigest, vulnJSON); err != nil {
|
||||
slog.Error("Failed to upload vulnerability report blob",
|
||||
// Upload SBOM as a blob to the hold's PDS blob storage (like manifest blobs)
|
||||
var sbomBlob *lexutil.LexBlob
|
||||
if msg.SBOM != "" {
|
||||
blob, err := uploadBlobToStorage(ctx, sb.driver, sb.holdDID, []byte(msg.SBOM), "application/spdx+json")
|
||||
if err != nil {
|
||||
slog.Error("Failed to upload SBOM blob to PDS storage",
|
||||
"seq", msg.Seq,
|
||||
"error", err)
|
||||
}
|
||||
|
||||
// Build and store ORAS manifest
|
||||
if msg.Summary != nil {
|
||||
if err := sb.storeORASManifest(ctx, manifestDigest, repository, userDID, vulnDigest, vulnJSON, *msg.Summary); err != nil {
|
||||
slog.Error("Failed to store ORAS manifest",
|
||||
"seq", msg.Seq,
|
||||
"error", err)
|
||||
}
|
||||
} else {
|
||||
sbomBlob = blob
|
||||
}
|
||||
}
|
||||
|
||||
// Store SBOM blob if provided
|
||||
if msg.SBOM != "" {
|
||||
sbomJSON := []byte(msg.SBOM)
|
||||
sbomDigest := fmt.Sprintf("sha256:%x", sha256.Sum256(sbomJSON))
|
||||
// Store scan result as a record in the hold's embedded PDS
|
||||
if msg.Summary != nil {
|
||||
scanRecord := atproto.NewScanRecord(
|
||||
manifestDigest, repository, userDID,
|
||||
sbomBlob,
|
||||
msg.Summary.Critical, msg.Summary.High, msg.Summary.Medium, msg.Summary.Low, msg.Summary.Total,
|
||||
"atcr-scanner-v1.0.0",
|
||||
)
|
||||
|
||||
if err := sb.uploadBlob(ctx, sbomDigest, sbomJSON); err != nil {
|
||||
slog.Error("Failed to upload SBOM blob",
|
||||
rpath, _, err := sb.pds.CreateScanRecord(ctx, scanRecord)
|
||||
if err != nil {
|
||||
slog.Error("Failed to store scan record in PDS",
|
||||
"seq", msg.Seq,
|
||||
"error", err)
|
||||
} else {
|
||||
slog.Info("Scan record stored in PDS",
|
||||
"rpath", rpath,
|
||||
"manifest", scanRecord.Manifest,
|
||||
"critical", msg.Summary.Critical,
|
||||
"high", msg.Summary.High,
|
||||
"total", msg.Summary.Total)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,99 +589,6 @@ func (sb *ScanBroadcaster) ValidateScannerSecret(secret string) bool {
|
||||
return sb.secret != "" && secret == sb.secret
|
||||
}
|
||||
|
||||
// storeORASManifest creates an ORAS vulnerability manifest as a blob in S3
|
||||
// The ORAS manifest's "subject" field references the original manifest by digest,
|
||||
// enabling OCI referrers API discovery.
|
||||
func (sb *ScanBroadcaster) storeORASManifest(ctx context.Context, manifestDigest, repository, userDID, vulnDigest string, vulnJSON []byte, summary VulnerabilitySummary) error {
|
||||
scannerVersion := "atcr-scanner-v1.0.0"
|
||||
|
||||
// Create ORAS manifest
|
||||
orasManifest := map[string]interface{}{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"artifactType": "application/vnd.atcr.vulnerabilities+json",
|
||||
"config": map[string]interface{}{
|
||||
"mediaType": "application/vnd.oci.empty.v1+json",
|
||||
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
"size": 2,
|
||||
},
|
||||
"subject": map[string]interface{}{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": manifestDigest,
|
||||
"size": 0,
|
||||
},
|
||||
"layers": []map[string]interface{}{
|
||||
{
|
||||
"mediaType": "application/json",
|
||||
"digest": vulnDigest,
|
||||
"size": len(vulnJSON),
|
||||
"annotations": map[string]string{
|
||||
"org.opencontainers.image.title": "vulnerability-report.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
"annotations": map[string]string{
|
||||
"io.atcr.vuln.critical": fmt.Sprintf("%d", summary.Critical),
|
||||
"io.atcr.vuln.high": fmt.Sprintf("%d", summary.High),
|
||||
"io.atcr.vuln.medium": fmt.Sprintf("%d", summary.Medium),
|
||||
"io.atcr.vuln.low": fmt.Sprintf("%d", summary.Low),
|
||||
"io.atcr.vuln.total": fmt.Sprintf("%d", summary.Total),
|
||||
"io.atcr.vuln.scannedAt": time.Now().Format(time.RFC3339),
|
||||
"io.atcr.vuln.scannerVersion": scannerVersion,
|
||||
"io.atcr.vuln.repository": repository,
|
||||
"io.atcr.vuln.ownerDid": userDID,
|
||||
"io.atcr.vuln.holdDid": sb.holdDID,
|
||||
},
|
||||
}
|
||||
|
||||
orasManifestJSON, err := json.Marshal(orasManifest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode ORAS manifest: %w", err)
|
||||
}
|
||||
|
||||
orasHash := sha256.Sum256(orasManifestJSON)
|
||||
orasDigest := fmt.Sprintf("sha256:%x", orasHash)
|
||||
|
||||
// Upload ORAS manifest blob to S3
|
||||
if err := sb.uploadBlob(ctx, orasDigest, orasManifestJSON); err != nil {
|
||||
return fmt.Errorf("failed to upload ORAS manifest blob: %w", err)
|
||||
}
|
||||
|
||||
slog.Info("ORAS manifest stored",
|
||||
"digest", orasDigest,
|
||||
"repository", repository,
|
||||
"userDid", userDID,
|
||||
"critical", summary.Critical,
|
||||
"high", summary.High,
|
||||
"total", summary.Total)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// uploadBlob uploads a blob to S3 storage
|
||||
func (sb *ScanBroadcaster) uploadBlob(ctx context.Context, digest string, data []byte) error {
|
||||
digestHex := digest[len("sha256:"):]
|
||||
if len(digestHex) < 2 {
|
||||
return fmt.Errorf("invalid digest: %s", digest)
|
||||
}
|
||||
|
||||
blobPath := fmt.Sprintf("/docker/registry/v2/blobs/sha256/%s/%s/data",
|
||||
digestHex[:2], digestHex)
|
||||
|
||||
writer, err := sb.driver.Writer(ctx, blobPath, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create storage writer: %w", err)
|
||||
}
|
||||
defer writer.Close()
|
||||
|
||||
if _, err := writer.Write(data); err != nil {
|
||||
writer.Cancel(ctx)
|
||||
return fmt.Errorf("failed to write blob data: %w", err)
|
||||
}
|
||||
|
||||
return writer.Commit(ctx)
|
||||
}
|
||||
|
||||
func generateSubscriberID() string {
|
||||
b := make([]byte, 8)
|
||||
rand.Read(b)
|
||||
|
||||
@@ -29,6 +29,7 @@ func init() {
|
||||
lexutil.RegisterType(atproto.LayerCollection, &atproto.LayerRecord{})
|
||||
lexutil.RegisterType(atproto.TangledProfileCollection, &atproto.TangledProfileRecord{})
|
||||
lexutil.RegisterType(atproto.StatsCollection, &atproto.StatsRecord{})
|
||||
lexutil.RegisterType(atproto.ScanCollection, &atproto.ScanRecord{})
|
||||
}
|
||||
|
||||
// HoldPDS is a minimal ATProto PDS implementation for a hold service
|
||||
|
||||
Reference in New Issue
Block a user