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.
This commit is contained in:
2026-08-06 13:17:44 +01:00
parent 3a6d24fe0e
commit a92c3190c2
3 changed files with 163 additions and 0 deletions
+48
View File
@@ -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 {
+46
View File
@@ -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 {
+69
View File
@@ -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 {