From a92c3190c2cd391a4f50cd3e988c8704792ce299 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Thu, 6 Aug 2026 13:17:44 +0100 Subject: [PATCH] feat: ReportPackages wire types with hash short-circuit The pb packages are hand-written, not protoc-generated, and the wire codec is JSON (encoding.RegisterCodec(JSONCodec{})). Field numbers in the .proto are documentation; JSON field names are the contract. Both pb packages edited by hand to match. SyncResponse.collect_packages is omitempty and absent decodes as false, so an older server leaves agents collecting nothing rather than collecting without a licence. --- agent/internal/grpc/pb/vantage.pb.go | 48 +++++++++++++++++++ proto/vantage/v1/vantage.proto | 46 ++++++++++++++++++ server/internal/grpc/pb/vantage.pb.go | 69 +++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/agent/internal/grpc/pb/vantage.pb.go b/agent/internal/grpc/pb/vantage.pb.go index 94cec48..bfd3548 100644 --- a/agent/internal/grpc/pb/vantage.pb.go +++ b/agent/internal/grpc/pb/vantage.pb.go @@ -30,6 +30,45 @@ type SyncRequest struct { type SyncResponse struct { PublicKeys []string `json:"public_keys"` + // CollectPackages tells the agent whether this instance's licence grants + // vulnerability scanning. Absent decodes as false, which is the safe + // direction: an older server leaves agents collecting nothing. + CollectPackages bool `json:"collect_packages,omitempty"` +} + +type OSRelease struct { + Family string `json:"family"` + // VersionId is not optional: Ubuntu 22.04 and 24.04 publish different fixed + // versions for the same CVE, so a scan without it is guesswork. + VersionId string `json:"version_id"` + Arch string `json:"arch,omitempty"` +} + +type InstalledPackage struct { + Name string `json:"name"` + Version string `json:"version"` + Epoch int32 `json:"epoch,omitempty"` + Arch string `json:"arch,omitempty"` + // SourceName is what the Debian and Ubuntu feeds are keyed on: one advisory + // against "openssl" covers libssl3, openssl and libssl-dev. + SourceName string `json:"source_name,omitempty"` +} + +// ReportPackagesRequest carries a server's installed package set. +// +// The agent calls twice at most: first with Packages empty, offering only the +// hash. If the server already holds it, NeedFull is false and the ~150KB body +// is never sent. +type ReportPackagesRequest struct { + ServerId string `json:"server_id"` + AgentToken string `json:"agent_token"` + Hash string `json:"hash"` + Os OSRelease `json:"os"` + Packages []InstalledPackage `json:"packages,omitempty"` +} + +type ReportPackagesResponse struct { + NeedFull bool `json:"need_full"` } type UploadKeyRequest struct { @@ -337,6 +376,7 @@ type VantageClient interface { SyncKeys(ctx context.Context, in *SyncRequest, opts ...grpc.CallOption) (*SyncResponse, error) UploadGeneratedKey(ctx context.Context, in *UploadKeyRequest, opts ...grpc.CallOption) (*UploadKeyResponse, error) ReportUpdates(ctx context.Context, in *ReportUpdatesRequest, opts ...grpc.CallOption) (*ReportUpdatesResponse, error) + ReportPackages(ctx context.Context, in *ReportPackagesRequest, opts ...grpc.CallOption) (*ReportPackagesResponse, error) ReportInventory(ctx context.Context, in *InventoryReport, opts ...grpc.CallOption) (*InventoryReportResponse, error) SyncMonitors(ctx context.Context, in *SyncMonitorsRequest, opts ...grpc.CallOption) (*SyncMonitorsResponse, error) ReportChecks(ctx context.Context, in *ReportChecksRequest, opts ...grpc.CallOption) (*ReportChecksResponse, error) @@ -396,6 +436,14 @@ func (c *keyManagerClient) ReportUpdates(ctx context.Context, in *ReportUpdatesR return out, nil } +func (c *keyManagerClient) ReportPackages(ctx context.Context, in *ReportPackagesRequest, opts ...grpc.CallOption) (*ReportPackagesResponse, error) { + out := new(ReportPackagesResponse) + if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportPackages", in, out, opts...); err != nil { + return nil, err + } + return out, nil +} + func (c *keyManagerClient) ReportInventory(ctx context.Context, in *InventoryReport, opts ...grpc.CallOption) (*InventoryReportResponse, error) { out := new(InventoryReportResponse) if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportInventory", in, out, opts...); err != nil { diff --git a/proto/vantage/v1/vantage.proto b/proto/vantage/v1/vantage.proto index 9d0b0d2..245e7bf 100644 --- a/proto/vantage/v1/vantage.proto +++ b/proto/vantage/v1/vantage.proto @@ -9,6 +9,7 @@ service Vantage { rpc SyncKeys(SyncRequest) returns (SyncResponse); rpc UploadGeneratedKey(UploadKeyRequest) returns (UploadKeyResponse); rpc ReportUpdates(ReportUpdatesRequest) returns (ReportUpdatesResponse); + rpc ReportPackages(ReportPackagesRequest) returns (ReportPackagesResponse); rpc ReportInventory(InventoryReport) returns (InventoryReportResponse); rpc SyncMonitors(SyncMonitorsRequest) returns (SyncMonitorsResponse); rpc ReportChecks(ReportChecksRequest) returns (ReportChecksResponse); @@ -37,6 +38,51 @@ message SyncRequest { message SyncResponse { repeated string public_keys = 1; + + // collect_packages tells the agent whether this instance's licence grants + // vulnerability scanning. False means do not collect at all: no gRPC body, + // no document, no storage. The server re-checks on ReportPackages — this + // flag is the optimisation, the server check is the boundary. + // + // Absent reads as false, which is the safe direction: an old server that + // does not send it leaves agents collecting nothing. + bool collect_packages = 2; +} + +// ReportPackages carries a server's installed package set. +// +// The agent calls twice at most. The first call sends only the hash; if the +// server already holds that hash it answers need_full = false and the ~150KB +// body is never sent. A machine's package set changes rarely, so almost every +// hour costs one small message. +message ReportPackagesRequest { + string server_id = 1; + string agent_token = 2; + string hash = 3; + OSRelease os = 4; + repeated InstalledPackage packages = 5; // empty on the offer call +} + +message ReportPackagesResponse { + bool need_full = 1; +} + +message OSRelease { + string family = 1; + // version_id is not optional: Ubuntu 22.04 and 24.04 publish different fixed + // versions for the same CVE, so a scan without it is guesswork. + string version_id = 2; + string arch = 3; +} + +message InstalledPackage { + string name = 1; + string version = 2; + int32 epoch = 3; + string arch = 4; + // source_name is what the Debian and Ubuntu feeds are keyed on: one advisory + // against "openssl" covers libssl3, openssl and libssl-dev. + string source_name = 5; } message UploadKeyRequest { diff --git a/server/internal/grpc/pb/vantage.pb.go b/server/internal/grpc/pb/vantage.pb.go index 7237d29..0cd42d9 100644 --- a/server/internal/grpc/pb/vantage.pb.go +++ b/server/internal/grpc/pb/vantage.pb.go @@ -28,6 +28,45 @@ type SyncRequest struct { type SyncResponse struct { PublicKeys []string `json:"public_keys"` + // CollectPackages tells the agent whether this instance's licence grants + // vulnerability scanning. Absent decodes as false, which is the safe + // direction: an older server leaves agents collecting nothing. + CollectPackages bool `json:"collect_packages,omitempty"` +} + +type OSRelease struct { + Family string `json:"family"` + // VersionId is not optional: Ubuntu 22.04 and 24.04 publish different fixed + // versions for the same CVE, so a scan without it is guesswork. + VersionId string `json:"version_id"` + Arch string `json:"arch,omitempty"` +} + +type InstalledPackage struct { + Name string `json:"name"` + Version string `json:"version"` + Epoch int32 `json:"epoch,omitempty"` + Arch string `json:"arch,omitempty"` + // SourceName is what the Debian and Ubuntu feeds are keyed on: one advisory + // against "openssl" covers libssl3, openssl and libssl-dev. + SourceName string `json:"source_name,omitempty"` +} + +// ReportPackagesRequest carries a server's installed package set. +// +// The agent calls twice at most: first with Packages empty, offering only the +// hash. If the server already holds it, NeedFull is false and the ~150KB body +// is never sent. +type ReportPackagesRequest struct { + ServerId string `json:"server_id"` + AgentToken string `json:"agent_token"` + Hash string `json:"hash"` + Os OSRelease `json:"os"` + Packages []InstalledPackage `json:"packages,omitempty"` +} + +type ReportPackagesResponse struct { + NeedFull bool `json:"need_full"` } type UploadKeyRequest struct { @@ -326,6 +365,7 @@ type VantageServer interface { SyncKeys(context.Context, *SyncRequest) (*SyncResponse, error) UploadGeneratedKey(context.Context, *UploadKeyRequest) (*UploadKeyResponse, error) ReportUpdates(context.Context, *ReportUpdatesRequest) (*ReportUpdatesResponse, error) + ReportPackages(context.Context, *ReportPackagesRequest) (*ReportPackagesResponse, error) ReportInventory(context.Context, *InventoryReport) (*InventoryReportResponse, error) SyncMonitors(context.Context, *SyncMonitorsRequest) (*SyncMonitorsResponse, error) ReportChecks(context.Context, *ReportChecksRequest) (*ReportChecksResponse, error) @@ -351,6 +391,10 @@ func (UnimplementedVantageServer) ReportUpdates(context.Context, *ReportUpdatesR return nil, status.Errorf(codes.Unimplemented, "method ReportUpdates not implemented") } +func (UnimplementedVantageServer) ReportPackages(context.Context, *ReportPackagesRequest) (*ReportPackagesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ReportPackages not implemented") +} + func (UnimplementedVantageServer) ReportInventory(context.Context, *InventoryReport) (*InventoryReportResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ReportInventory not implemented") } @@ -376,6 +420,7 @@ type VantageClient interface { SyncKeys(ctx context.Context, in *SyncRequest, opts ...grpc.CallOption) (*SyncResponse, error) UploadGeneratedKey(ctx context.Context, in *UploadKeyRequest, opts ...grpc.CallOption) (*UploadKeyResponse, error) ReportUpdates(ctx context.Context, in *ReportUpdatesRequest, opts ...grpc.CallOption) (*ReportUpdatesResponse, error) + ReportPackages(ctx context.Context, in *ReportPackagesRequest, opts ...grpc.CallOption) (*ReportPackagesResponse, error) ReportInventory(ctx context.Context, in *InventoryReport, opts ...grpc.CallOption) (*InventoryReportResponse, error) SyncMonitors(ctx context.Context, in *SyncMonitorsRequest, opts ...grpc.CallOption) (*SyncMonitorsResponse, error) ReportChecks(ctx context.Context, in *ReportChecksRequest, opts ...grpc.CallOption) (*ReportChecksResponse, error) @@ -423,6 +468,14 @@ func (c *keyManagerClient) ReportUpdates(ctx context.Context, in *ReportUpdatesR return out, nil } +func (c *keyManagerClient) ReportPackages(ctx context.Context, in *ReportPackagesRequest, opts ...grpc.CallOption) (*ReportPackagesResponse, error) { + out := new(ReportPackagesResponse) + if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportPackages", in, out, opts...); err != nil { + return nil, err + } + return out, nil +} + func (c *keyManagerClient) ReportInventory(ctx context.Context, in *InventoryReport, opts ...grpc.CallOption) (*InventoryReportResponse, error) { out := new(InventoryReportResponse) if err := c.cc.Invoke(ctx, "/vantage.v1.Vantage/ReportInventory", in, out, opts...); err != nil { @@ -475,6 +528,7 @@ var Vantage_ServiceDesc = grpc.ServiceDesc{ {MethodName: "SyncKeys", Handler: _Vantage_SyncKeys_Handler}, {MethodName: "UploadGeneratedKey", Handler: _Vantage_UploadGeneratedKey_Handler}, {MethodName: "ReportUpdates", Handler: _Vantage_ReportUpdates_Handler}, + {MethodName: "ReportPackages", Handler: _Vantage_ReportPackages_Handler}, {MethodName: "ReportInventory", Handler: _Vantage_ReportInventory_Handler}, {MethodName: "SyncMonitors", Handler: _Vantage_SyncMonitors_Handler}, {MethodName: "ReportChecks", Handler: _Vantage_ReportChecks_Handler}, @@ -556,6 +610,21 @@ func _Vantage_ReportUpdates_Handler(srv interface{}, ctx context.Context, dec fu return interceptor(ctx, in, info, handler) } +func _Vantage_ReportPackages_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReportPackagesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VantageServer).ReportPackages(ctx, in) + } + info := &grpc.UnaryServerInfo{Server: srv, FullMethod: "/vantage.v1.Vantage/ReportPackages"} + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VantageServer).ReportPackages(ctx, req.(*ReportPackagesRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Vantage_ReportInventory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(InventoryReport) if err := dec(in); err != nil {