diff --git a/api/grpc/grpc.go b/api/grpc/grpc.go index bbefdd30e..550253849 100644 --- a/api/grpc/grpc.go +++ b/api/grpc/grpc.go @@ -28,7 +28,12 @@ func init() { } // NewAPI creates a new gRPC API instantiation -func NewAPI(config Config) API { +func NewAPI(opts ...ConfigOpts) API { + var config Config + for _, opt := range opts { + opt(&config) + } + return &apiImpl{ config: config, } @@ -56,7 +61,7 @@ func (a *apiImpl) connectToLocalEndpoint() (*grpc.ClientConn, error) { } func (a *apiImpl) Start() { - grpcServer := grpc.NewServer(grpcmiddleware.WithUnaryServerChain(a.unaryInterceptors()...)) + grpcServer := grpc.NewServer(grpcmiddleware.WithUnaryServerChain(a.config.UnaryInterceptors...)) for _, serv := range a.apiServices { serv.RegisterServiceServer(grpcServer) } @@ -72,26 +77,29 @@ func (a *apiImpl) Start() { gwHandler := a.muxer(conn) - addr := fmt.Sprintf(":%d", a.config.Port) - log.Infof("Listening on public endpoint: %v", addr) - lis, err := net.Listen("tcp", addr) - if err != nil { - log.Fatal(err) - } - conf, err := mtls.TLSServerConfig() - if err != nil { - log.Fatal(err) - } - - lis = tls.NewListener(lis, conf) - handler := httpGrpcRouter(grpcServer, gwHandler) - go func() { - server := http.Server{ - Handler: handler, - ErrorLog: golog.New(httpErrorLogger{}, "", golog.LstdFlags), + var publicListener net.Listener + if a.config.PublicEndpoint { + addr := fmt.Sprintf(":%d", a.config.Port) + log.Infof("Listening on public endpoint: %v", addr) + lis, err := net.Listen("tcp", addr) + if err != nil { + log.Fatal(err) } - log.Fatal(server.Serve(lis)) - }() + conf, err := mtls.TLSServerConfig() + if err != nil { + log.Fatal(err) + } + + publicListener = tls.NewListener(lis, conf) + handler := httpGrpcRouter(grpcServer, gwHandler) + go func() { + server := http.Server{ + Handler: handler, + ErrorLog: golog.New(httpErrorLogger{}, "", golog.LstdFlags), + } + log.Fatal(server.Serve(publicListener)) + }() + } } // APIService is the service interface @@ -115,24 +123,55 @@ type apiImpl struct { // A Config configures the server. type Config struct { - Port int - CustomRoutes map[string]http.Handler + Port int + CustomRoutes map[string]http.Handler + UnaryInterceptors []grpc.UnaryServerInterceptor + PublicEndpoint bool } -func (a *apiImpl) Register(services ...APIService) { - a.apiServices = append(a.apiServices, services...) +// ConfigOpts defines configurations to start a gRPC server. +type ConfigOpts func(cfg *Config) + +// WithTLSEndpoint starts the gRPC server with TLS enabled on port. +func WithTLSEndpoint(port int) ConfigOpts { + return func(cfg *Config) { + cfg.Port = port + cfg.PublicEndpoint = true + } +} + +// WithDefaultInterceptors should be used when starting Scanner API. This interceptors list contains interceptors +// that check method availability on slim mode and TLS client checks. +func WithDefaultInterceptors() ConfigOpts { + return func(cfg *Config) { + // Interceptors are executed in order. + cfg.UnaryInterceptors = []grpc.UnaryServerInterceptor{ + // Ensure the user is authorized before doing anything else. + verifyPeerCertsUnaryServerInterceptor(), + slimModeUnaryServerInterceptor(), + grpcprometheus.UnaryServerInterceptor, + } + } } -func (a *apiImpl) unaryInterceptors() []grpc.UnaryServerInterceptor { - // Interceptors are executed in order. - return []grpc.UnaryServerInterceptor{ - // Ensure the user is authorized before doing anything else. - verifyPeerCertsUnaryServerInterceptor(), - slimModeUnaryServerInterceptor(), - grpcprometheus.UnaryServerInterceptor, +// WithCustomUnaryInterceptors should be used to set custom GRPC interceptors. +func WithCustomUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) ConfigOpts { + return func(cfg *Config) { + cfg.UnaryInterceptors = interceptors } } +// WithCustomRoutes sets custom HTTP routes. +func WithCustomRoutes(routes map[string]http.Handler) ConfigOpts { + return func(cfg *Config) { + cfg.CustomRoutes = routes + } +} + +func (a *apiImpl) Register(services ...APIService) { + a.apiServices = append(a.apiServices, services...) +} + func (a *apiImpl) muxer(localConn *grpc.ClientConn) http.Handler { mux := http.NewServeMux() for route, handler := range a.config.CustomRoutes { diff --git a/api/v1/nodeinventory/service.go b/api/v1/nodeinventory/service.go new file mode 100644 index 000000000..f3354f8b2 --- /dev/null +++ b/api/v1/nodeinventory/service.go @@ -0,0 +1,59 @@ +package nodeinventory + +import ( + "context" + + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + apiGRPC "github.com/stackrox/scanner/api/grpc" + v1 "github.com/stackrox/scanner/generated/scanner/api/v1" + "github.com/stackrox/scanner/pkg/nodeinventory" + "google.golang.org/grpc" +) + +// Service defines the node scanning service. +type Service interface { + apiGRPC.APIService + + v1.NodeInventoryServiceServer +} + +// NewService returns the service for node scanning +func NewService(nodeName string) Service { + return &serviceImpl{ + inventoryCollector: &nodeinventory.Scanner{}, + nodeName: nodeName, + } +} + +type serviceImpl struct { + inventoryCollector *nodeinventory.Scanner + nodeName string +} + +func (s *serviceImpl) GetNodeInventory(ctx context.Context, req *v1.GetNodeInventoryRequest) (*v1.GetNodeInventoryResponse, error) { + inventoryScan, err := s.inventoryCollector.Scan(s.nodeName) + if err != nil { + log.Errorf("Error running inventoryCollector.Scan(%s): %v", s.nodeName, err) + return nil, errors.New("Internal scanner error: failed to scan node") + } + + log.Debugf("InventoryScan: %+v", inventoryScan) + + return &v1.GetNodeInventoryResponse{ + NodeName: s.nodeName, + Components: inventoryScan.Components, + Notes: inventoryScan.Notes, + }, nil +} + +// RegisterServiceServer registers this service with the given gRPC Server. +func (s *serviceImpl) RegisterServiceServer(grpcServer *grpc.Server) { + v1.RegisterNodeInventoryServiceServer(grpcServer, s) +} + +// RegisterServiceHandler registers this service with the given gRPC Gateway endpoint. +func (s *serviceImpl) RegisterServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return v1.RegisterNodeInventoryServiceHandler(ctx, mux, conn) +} diff --git a/cmd/clair/main.go b/cmd/clair/main.go index 362972634..813e13a73 100644 --- a/cmd/clair/main.go +++ b/cmd/clair/main.go @@ -27,12 +27,14 @@ import ( "strings" "time" + grpcprometheus "github.com/grpc-ecosystem/go-grpc-prometheus" log "github.com/sirupsen/logrus" "github.com/stackrox/rox/pkg/httputil/proxy" "github.com/stackrox/rox/pkg/sync" "github.com/stackrox/scanner/api" "github.com/stackrox/scanner/api/grpc" "github.com/stackrox/scanner/api/v1/imagescan" + "github.com/stackrox/scanner/api/v1/nodeinventory" "github.com/stackrox/scanner/api/v1/nodescan" "github.com/stackrox/scanner/api/v1/orchestratorscan" "github.com/stackrox/scanner/api/v1/ping" @@ -180,10 +182,10 @@ func Boot(config *Config, slimMode bool) { serv := server.New(fmt.Sprintf(":%d", config.API.HTTPSPort), db) go api.RunClairify(serv) - grpcAPI := grpc.NewAPI(grpc.Config{ - Port: config.API.GRPCPort, - CustomRoutes: debugRoutes, - }) + grpcAPI := grpc.NewAPI( + grpc.WithTLSEndpoint(config.API.GRPCPort), + grpc.WithDefaultInterceptors(), + grpc.WithCustomRoutes(debugRoutes)) grpcAPI.Register( ping.NewService(), @@ -201,14 +203,34 @@ func Boot(config *Config, slimMode bool) { serv.Close() } +func bootNodeInventoryScanner() { + if env.NodeName.Value() == "" { + log.Errorf("Cannot start node inventory scanner when %s isn't set. Make sure the environment varialbe is set from value spec.nodeName in Kubernetes", + env.NodeName.EnvVar()) + } + + grpcAPI := grpc.NewAPI( + grpc.WithCustomRoutes(debugRoutes), + grpc.WithCustomUnaryInterceptors(grpcprometheus.UnaryServerInterceptor)) + + grpcAPI.Register( + ping.NewService(), + nodeinventory.NewService(env.NodeName.Value()), + ) + go grpcAPI.Start() + + // Wait for interruption and shutdown gracefully. + waitForSignals(os.Interrupt, unix.SIGTERM) + log.Info("Received interruption, gracefully stopping ...") +} + func main() { // Parse command-line arguments flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) flagConfigPath := flag.String("config", "/etc/scanner/config.yaml", "Load configuration from the specified file.") + flagNodeInventoryMode := flag.Bool("nodeinventory", false, "Run Scanner binary in Node Inventory mode (this should only be used in Secured Clusters)") flag.Parse() - proxy.WatchProxyConfig(context.Background(), proxyConfigPath, proxyConfigFile, true) - // Check for dependencies. for _, bin := range []string{"rpm", "xz"} { _, err := exec.LookPath(bin) @@ -254,6 +276,14 @@ func main() { // Cleanup any residue temporary files. ioutils.CleanUpTempFiles() + if *flagNodeInventoryMode { + log.Infof("Running Scanner version %s in Node Inventory mode", version.Version) + bootNodeInventoryScanner() + return + } + + proxy.WatchProxyConfig(context.Background(), proxyConfigPath, proxyConfigFile, true) + slimMode := env.SlimMode.Enabled() scannerName := "Scanner" diff --git a/generated/scanner/api/v1/node_inventory_service.pb.go b/generated/scanner/api/v1/node_inventory_service.pb.go new file mode 100644 index 000000000..a5acac2fa --- /dev/null +++ b/generated/scanner/api/v1/node_inventory_service.pb.go @@ -0,0 +1,744 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: scanner/api/v1/node_inventory_service.proto + +package scannerV1 + +import ( + context "context" + fmt "fmt" + proto "github.com/golang/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type GetNodeInventoryResponse struct { + NodeName string `protobuf:"bytes,1,opt,name=node_name,json=nodeName,proto3" json:"node_name,omitempty"` + Components *Components `protobuf:"bytes,2,opt,name=components,proto3" json:"components,omitempty"` + Notes []Note `protobuf:"varint,3,rep,packed,name=notes,proto3,enum=scannerV1.Note" json:"notes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetNodeInventoryResponse) Reset() { *m = GetNodeInventoryResponse{} } +func (m *GetNodeInventoryResponse) String() string { return proto.CompactTextString(m) } +func (*GetNodeInventoryResponse) ProtoMessage() {} +func (*GetNodeInventoryResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_51a6ae79358c6b21, []int{0} +} +func (m *GetNodeInventoryResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GetNodeInventoryResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GetNodeInventoryResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GetNodeInventoryResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetNodeInventoryResponse.Merge(m, src) +} +func (m *GetNodeInventoryResponse) XXX_Size() int { + return m.Size() +} +func (m *GetNodeInventoryResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetNodeInventoryResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetNodeInventoryResponse proto.InternalMessageInfo + +func (m *GetNodeInventoryResponse) GetNodeName() string { + if m != nil { + return m.NodeName + } + return "" +} + +func (m *GetNodeInventoryResponse) GetComponents() *Components { + if m != nil { + return m.Components + } + return nil +} + +func (m *GetNodeInventoryResponse) GetNotes() []Note { + if m != nil { + return m.Notes + } + return nil +} + +func (m *GetNodeInventoryResponse) MessageClone() proto.Message { + return m.Clone() +} +func (m *GetNodeInventoryResponse) Clone() *GetNodeInventoryResponse { + if m == nil { + return nil + } + cloned := new(GetNodeInventoryResponse) + *cloned = *m + + cloned.Components = m.Components.Clone() + if m.Notes != nil { + cloned.Notes = make([]Note, len(m.Notes)) + copy(cloned.Notes, m.Notes) + } + return cloned +} + +type GetNodeInventoryRequest struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetNodeInventoryRequest) Reset() { *m = GetNodeInventoryRequest{} } +func (m *GetNodeInventoryRequest) String() string { return proto.CompactTextString(m) } +func (*GetNodeInventoryRequest) ProtoMessage() {} +func (*GetNodeInventoryRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_51a6ae79358c6b21, []int{1} +} +func (m *GetNodeInventoryRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GetNodeInventoryRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GetNodeInventoryRequest.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GetNodeInventoryRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetNodeInventoryRequest.Merge(m, src) +} +func (m *GetNodeInventoryRequest) XXX_Size() int { + return m.Size() +} +func (m *GetNodeInventoryRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetNodeInventoryRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetNodeInventoryRequest proto.InternalMessageInfo + +func (m *GetNodeInventoryRequest) MessageClone() proto.Message { + return m.Clone() +} +func (m *GetNodeInventoryRequest) Clone() *GetNodeInventoryRequest { + if m == nil { + return nil + } + cloned := new(GetNodeInventoryRequest) + *cloned = *m + + return cloned +} + +func init() { + proto.RegisterType((*GetNodeInventoryResponse)(nil), "scannerV1.GetNodeInventoryResponse") + proto.RegisterType((*GetNodeInventoryRequest)(nil), "scannerV1.GetNodeInventoryRequest") +} + +func init() { + proto.RegisterFile("scanner/api/v1/node_inventory_service.proto", fileDescriptor_51a6ae79358c6b21) +} + +var fileDescriptor_51a6ae79358c6b21 = []byte{ + // 309 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x91, 0xc1, 0x4a, 0xf3, 0x40, + 0x14, 0x85, 0x3b, 0x7f, 0xf9, 0xc5, 0x8e, 0xa0, 0x32, 0x28, 0xa6, 0x51, 0x42, 0x88, 0x08, 0x05, + 0x61, 0x4a, 0x2b, 0x2e, 0xdd, 0xe8, 0x42, 0xdc, 0x74, 0x11, 0x41, 0x44, 0x90, 0x32, 0xa6, 0x97, + 0x32, 0x68, 0xef, 0x8d, 0x99, 0x69, 0xd0, 0x07, 0x11, 0x7c, 0x24, 0x97, 0x3e, 0x82, 0xd4, 0x17, + 0x91, 0x66, 0xda, 0x18, 0x5b, 0x74, 0x7b, 0xcf, 0x39, 0x1f, 0x87, 0x73, 0xf9, 0xa1, 0x49, 0x14, + 0x22, 0x64, 0x6d, 0x95, 0xea, 0x76, 0xde, 0x69, 0x23, 0x0d, 0xa0, 0xaf, 0x31, 0x07, 0xb4, 0x94, + 0x3d, 0xf7, 0x0d, 0x64, 0xb9, 0x4e, 0x40, 0xa6, 0x19, 0x59, 0x12, 0x8d, 0x99, 0xf9, 0xaa, 0xe3, + 0xef, 0x0d, 0x89, 0x86, 0x0f, 0x50, 0xc4, 0x14, 0x22, 0x59, 0x65, 0x35, 0xa1, 0x71, 0x46, 0xbf, + 0xb9, 0x44, 0xb5, 0x33, 0x86, 0x1f, 0x2c, 0x48, 0x09, 0x8d, 0x52, 0x42, 0x40, 0xeb, 0xf4, 0xe8, + 0x85, 0x71, 0xef, 0x1c, 0x6c, 0x8f, 0x06, 0x70, 0x31, 0xaf, 0x11, 0x83, 0x49, 0x09, 0x0d, 0x88, + 0x5d, 0xde, 0x28, 0x0a, 0xa2, 0x1a, 0x81, 0xc7, 0x42, 0xd6, 0x6a, 0xc4, 0xab, 0xd3, 0x43, 0x4f, + 0x8d, 0x40, 0x1c, 0x73, 0x5e, 0xc2, 0x8c, 0xf7, 0x2f, 0x64, 0xad, 0xb5, 0xee, 0xb6, 0x2c, 0x2b, + 0xcb, 0xb3, 0x52, 0x8c, 0x2b, 0x46, 0x71, 0xc0, 0xff, 0x4f, 0xeb, 0x19, 0xaf, 0x1e, 0xd6, 0x5b, + 0xeb, 0xdd, 0x8d, 0x4a, 0xa2, 0x47, 0x16, 0x62, 0xa7, 0x46, 0x4d, 0xbe, 0xb3, 0x5c, 0xeb, 0x71, + 0x0c, 0xc6, 0x76, 0xc7, 0x7c, 0xeb, 0xc7, 0xfd, 0xd2, 0x8d, 0x26, 0x6e, 0xf9, 0xe6, 0x62, 0x44, + 0x44, 0x15, 0xfc, 0x2f, 0x3c, 0x7f, 0xff, 0x4f, 0x8f, 0x9b, 0x22, 0xaa, 0x9d, 0x9e, 0xbc, 0x4d, + 0x02, 0xf6, 0x3e, 0x09, 0xd8, 0xc7, 0x24, 0x60, 0xaf, 0x9f, 0x41, 0x8d, 0x87, 0x9a, 0xa4, 0xb1, + 0x2a, 0xb9, 0xcf, 0xe8, 0xc9, 0xcd, 0x29, 0x55, 0xaa, 0xe7, 0x34, 0x99, 0x77, 0x6e, 0xbe, 0x3f, + 0x78, 0x5d, 0xbb, 0x5b, 0x29, 0x2c, 0x47, 0x5f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xb8, 0x42, 0x87, + 0x03, 0x04, 0x02, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// NodeInventoryServiceClient is the client API for NodeInventoryService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConnInterface.NewStream. +type NodeInventoryServiceClient interface { + GetNodeInventory(ctx context.Context, in *GetNodeInventoryRequest, opts ...grpc.CallOption) (*GetNodeInventoryResponse, error) +} + +type nodeInventoryServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewNodeInventoryServiceClient(cc grpc.ClientConnInterface) NodeInventoryServiceClient { + return &nodeInventoryServiceClient{cc} +} + +func (c *nodeInventoryServiceClient) GetNodeInventory(ctx context.Context, in *GetNodeInventoryRequest, opts ...grpc.CallOption) (*GetNodeInventoryResponse, error) { + out := new(GetNodeInventoryResponse) + err := c.cc.Invoke(ctx, "/scannerV1.NodeInventoryService/GetNodeInventory", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NodeInventoryServiceServer is the server API for NodeInventoryService service. +type NodeInventoryServiceServer interface { + GetNodeInventory(context.Context, *GetNodeInventoryRequest) (*GetNodeInventoryResponse, error) +} + +// UnimplementedNodeInventoryServiceServer can be embedded to have forward compatible implementations. +type UnimplementedNodeInventoryServiceServer struct { +} + +func (*UnimplementedNodeInventoryServiceServer) GetNodeInventory(ctx context.Context, req *GetNodeInventoryRequest) (*GetNodeInventoryResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetNodeInventory not implemented") +} + +func RegisterNodeInventoryServiceServer(s *grpc.Server, srv NodeInventoryServiceServer) { + s.RegisterService(&_NodeInventoryService_serviceDesc, srv) +} + +func _NodeInventoryService_GetNodeInventory_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetNodeInventoryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NodeInventoryServiceServer).GetNodeInventory(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/scannerV1.NodeInventoryService/GetNodeInventory", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NodeInventoryServiceServer).GetNodeInventory(ctx, req.(*GetNodeInventoryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _NodeInventoryService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "scannerV1.NodeInventoryService", + HandlerType: (*NodeInventoryServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetNodeInventory", + Handler: _NodeInventoryService_GetNodeInventory_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "scanner/api/v1/node_inventory_service.proto", +} + +func (m *GetNodeInventoryResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetNodeInventoryResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GetNodeInventoryResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + if len(m.Notes) > 0 { + dAtA2 := make([]byte, len(m.Notes)*10) + var j1 int + for _, num := range m.Notes { + for num >= 1<<7 { + dAtA2[j1] = uint8(uint64(num)&0x7f | 0x80) + num >>= 7 + j1++ + } + dAtA2[j1] = uint8(num) + j1++ + } + i -= j1 + copy(dAtA[i:], dAtA2[:j1]) + i = encodeVarintNodeInventoryService(dAtA, i, uint64(j1)) + i-- + dAtA[i] = 0x1a + } + if m.Components != nil { + { + size, err := m.Components.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintNodeInventoryService(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + } + if len(m.NodeName) > 0 { + i -= len(m.NodeName) + copy(dAtA[i:], m.NodeName) + i = encodeVarintNodeInventoryService(dAtA, i, uint64(len(m.NodeName))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *GetNodeInventoryRequest) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GetNodeInventoryRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GetNodeInventoryRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.XXX_unrecognized != nil { + i -= len(m.XXX_unrecognized) + copy(dAtA[i:], m.XXX_unrecognized) + } + return len(dAtA) - i, nil +} + +func encodeVarintNodeInventoryService(dAtA []byte, offset int, v uint64) int { + offset -= sovNodeInventoryService(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *GetNodeInventoryResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.NodeName) + if l > 0 { + n += 1 + l + sovNodeInventoryService(uint64(l)) + } + if m.Components != nil { + l = m.Components.Size() + n += 1 + l + sovNodeInventoryService(uint64(l)) + } + if len(m.Notes) > 0 { + l = 0 + for _, e := range m.Notes { + l += sovNodeInventoryService(uint64(e)) + } + n += 1 + sovNodeInventoryService(uint64(l)) + l + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *GetNodeInventoryRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func sovNodeInventoryService(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozNodeInventoryService(x uint64) (n int) { + return sovNodeInventoryService(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *GetNodeInventoryResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetNodeInventoryResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetNodeInventoryResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field NodeName", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthNodeInventoryService + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthNodeInventoryService + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.NodeName = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Components", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthNodeInventoryService + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthNodeInventoryService + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Components == nil { + m.Components = &Components{} + } + if err := m.Components.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType == 0 { + var v Note + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= Note(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Notes = append(m.Notes, v) + } else if wireType == 2 { + var packedLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + packedLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if packedLen < 0 { + return ErrInvalidLengthNodeInventoryService + } + postIndex := iNdEx + packedLen + if postIndex < 0 { + return ErrInvalidLengthNodeInventoryService + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + var elementCount int + if elementCount != 0 && len(m.Notes) == 0 { + m.Notes = make([]Note, 0, elementCount) + } + for iNdEx < postIndex { + var v Note + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= Note(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Notes = append(m.Notes, v) + } + } else { + return fmt.Errorf("proto: wrong wireType = %d for field Notes", wireType) + } + default: + iNdEx = preIndex + skippy, err := skipNodeInventoryService(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthNodeInventoryService + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GetNodeInventoryRequest) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GetNodeInventoryRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GetNodeInventoryRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipNodeInventoryService(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthNodeInventoryService + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipNodeInventoryService(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowNodeInventoryService + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthNodeInventoryService + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupNodeInventoryService + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthNodeInventoryService + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthNodeInventoryService = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowNodeInventoryService = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupNodeInventoryService = fmt.Errorf("proto: unexpected end of group") +) diff --git a/generated/scanner/api/v1/node_inventory_service.pb.gw.go b/generated/scanner/api/v1/node_inventory_service.pb.gw.go new file mode 100644 index 000000000..8cef102f5 --- /dev/null +++ b/generated/scanner/api/v1/node_inventory_service.pb.gw.go @@ -0,0 +1,153 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: scanner/api/v1/node_inventory_service.proto + +/* +Package scannerV1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package scannerV1 + +import ( + "context" + "io" + "net/http" + + "github.com/golang/protobuf/descriptor" + "github.com/golang/protobuf/proto" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/grpc-ecosystem/grpc-gateway/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = descriptor.ForMessage +var _ = metadata.Join + +func request_NodeInventoryService_GetNodeInventory_0(ctx context.Context, marshaler runtime.Marshaler, client NodeInventoryServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetNodeInventoryRequest + var metadata runtime.ServerMetadata + + msg, err := client.GetNodeInventory(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_NodeInventoryService_GetNodeInventory_0(ctx context.Context, marshaler runtime.Marshaler, server NodeInventoryServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq GetNodeInventoryRequest + var metadata runtime.ServerMetadata + + msg, err := server.GetNodeInventory(ctx, &protoReq) + return msg, metadata, err + +} + +// RegisterNodeInventoryServiceHandlerServer registers the http handlers for service NodeInventoryService to "mux". +// UnaryRPC :call NodeInventoryServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterNodeInventoryServiceHandlerFromEndpoint instead. +func RegisterNodeInventoryServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server NodeInventoryServiceServer) error { + + mux.Handle("GET", pattern_NodeInventoryService_GetNodeInventory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_NodeInventoryService_GetNodeInventory_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_NodeInventoryService_GetNodeInventory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +// RegisterNodeInventoryServiceHandlerFromEndpoint is same as RegisterNodeInventoryServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterNodeInventoryServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterNodeInventoryServiceHandler(ctx, mux, conn) +} + +// RegisterNodeInventoryServiceHandler registers the http handlers for service NodeInventoryService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterNodeInventoryServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterNodeInventoryServiceHandlerClient(ctx, mux, NewNodeInventoryServiceClient(conn)) +} + +// RegisterNodeInventoryServiceHandlerClient registers the http handlers for service NodeInventoryService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "NodeInventoryServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "NodeInventoryServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "NodeInventoryServiceClient" to call the correct interceptors. +func RegisterNodeInventoryServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client NodeInventoryServiceClient) error { + + mux.Handle("GET", pattern_NodeInventoryService_GetNodeInventory_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_NodeInventoryService_GetNodeInventory_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_NodeInventoryService_GetNodeInventory_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_NodeInventoryService_GetNodeInventory_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v1", "nodescan", "scan"}, "", runtime.AssumeColonVerbOpt(false))) +) + +var ( + forward_NodeInventoryService_GetNodeInventory_0 = runtime.ForwardResponseMessage +) diff --git a/go.mod b/go.mod index 09a5f766a..683810a9f 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/stretchr/testify v1.8.2 go.etcd.io/bbolt v1.3.7 go.uber.org/ratelimit v0.2.0 + golang.org/x/exp v0.0.0-20220823124025-807a23277127 golang.org/x/sys v0.6.0 google.golang.org/api v0.112.0 google.golang.org/grpc v1.53.0 @@ -55,7 +56,6 @@ require ( github.com/containers/storage v1.45.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect ) require ( diff --git a/image/scanner/rhel/create-bundle.sh b/image/scanner/rhel/create-bundle.sh index 4af2ad269..993fee6b7 100755 --- a/image/scanner/rhel/create-bundle.sh +++ b/image/scanner/rhel/create-bundle.sh @@ -37,11 +37,11 @@ chmod -R 755 "${bundle_root}" # Copy scripts to image build context directory mkdir -p "${OUTPUT_DIR}/scripts" -cp "${INPUT_ROOT}/scripts/entrypoint.sh" "${OUTPUT_DIR}/scripts" -cp "${INPUT_ROOT}/scripts/import-additional-cas" "${OUTPUT_DIR}/scripts" -cp "${INPUT_ROOT}/scripts/restore-all-dir-contents" "${OUTPUT_DIR}/scripts" -cp "${INPUT_ROOT}/scripts/save-dir-contents" "${OUTPUT_DIR}/scripts" -cp "${INPUT_ROOT}/scripts/trust-root-ca" "${OUTPUT_DIR}/scripts" +cp "${INPUT_ROOT}/scripts/entrypoint.sh" "${OUTPUT_DIR}/scripts" +cp "${INPUT_ROOT}/scripts/import-additional-cas" "${OUTPUT_DIR}/scripts" +cp "${INPUT_ROOT}/scripts/restore-all-dir-contents" "${OUTPUT_DIR}/scripts" +cp "${INPUT_ROOT}/scripts/save-dir-contents" "${OUTPUT_DIR}/scripts" +cp "${INPUT_ROOT}/scripts/trust-root-ca" "${OUTPUT_DIR}/scripts" # ============================================================================= # Add binaries and data files to be included in the Dockerfile here. This diff --git a/pkg/env/booleansetting.go b/pkg/env/booleansetting.go index b987e6a9f..16153bce0 100644 --- a/pkg/env/booleansetting.go +++ b/pkg/env/booleansetting.go @@ -23,6 +23,6 @@ func (s *booleanSetting) Enabled() bool { // RegisterBooleanSetting globally registers and returns a new boolean setting. func RegisterBooleanSetting(envVar string, defaul bool, opts ...SettingOption) BooleanSetting { return &booleanSetting{ - Setting: registerSetting(envVar, append(opts, WithDefault(strconv.FormatBool(defaul)))...), + Setting: RegisterSetting(envVar, append(opts, WithDefault(strconv.FormatBool(defaul)))...), } } diff --git a/pkg/env/list.go b/pkg/env/list.go index b4650119a..0211ddd90 100644 --- a/pkg/env/list.go +++ b/pkg/env/list.go @@ -19,4 +19,8 @@ var ( // This is ignored if SkipPeerValidation is enabled. // This variable was copied over from the stackrox repo. OpenshiftAPI = RegisterBooleanSetting("ROX_OPENSHIFT_API", false) + + // NodeName is used when running Scanner in Node Inventory mode. This should be set by + // Kubernetes in the Secured Cluster. + NodeName = RegisterSetting("ROX_NODE_NAME") ) diff --git a/pkg/env/setting.go b/pkg/env/setting.go index 448be74b8..86ac89e7a 100644 --- a/pkg/env/setting.go +++ b/pkg/env/setting.go @@ -54,7 +54,8 @@ func AllowWithoutRox() SettingOption { }) } -func registerSetting(envVar string, opts ...SettingOption) Setting { +// RegisterSetting registers an environment variable with prefix ROX_. +func RegisterSetting(envVar string, opts ...SettingOption) Setting { if !strings.HasPrefix(envVar, "ROX_") { panic(fmt.Sprintf("invalid env var: %s, must start with ROX_", envVar)) } diff --git a/pkg/nodeinventory/inventorizer.go b/pkg/nodeinventory/inventorizer.go new file mode 100644 index 000000000..58c7ae308 --- /dev/null +++ b/pkg/nodeinventory/inventorizer.go @@ -0,0 +1,142 @@ +package nodeinventory + +import ( + "time" + + log "github.com/sirupsen/logrus" + "github.com/stackrox/rox/compliance/collection/metrics" + "github.com/stackrox/scanner/database" + scannerV1 "github.com/stackrox/scanner/generated/scanner/api/v1" + "github.com/stackrox/scanner/pkg/analyzer/nodes" + "golang.org/x/exp/maps" +) + +// Scanner is an implementation of NodeInventorizer +type Scanner struct { +} + +// ScanResult wraps scanned node components, scanner notes and the node name in a result message. +type ScanResult struct { + NodeName string + Components *scannerV1.Components + Notes []scannerV1.Note +} + +// Scan scans the current node and returns the results as ScanResult object +func (n *Scanner) Scan(nodeName string) (*ScanResult, error) { + metrics.ObserveScansTotal(nodeName) + startTime := time.Now() + + // uncertifiedRHEL is set to false, as scans are only supported on RHCOS for now, + // which only exists in certified versions + componentsHost, err := nodes.Analyze(nodeName, "/host/", nodes.AnalyzeOpts{UncertifiedRHEL: false, IsRHCOSRequired: true}) + + scanDuration := time.Since(startTime) + metrics.ObserveScanDuration(scanDuration, nodeName, err) + log.Debugf("Collecting Node Inventory took %f seconds", scanDuration.Seconds()) + + if err != nil { + log.Errorf("Error scanning node /host inventory: %v", err) + return nil, err + } + log.Debugf("Components found under /host: %v", componentsHost) + + protoComponents := protoComponentsFromScanComponents(componentsHost) + + return &ScanResult{ + NodeName: nodeName, + Components: protoComponents, + Notes: []scannerV1.Note{scannerV1.Note_LANGUAGE_CVES_UNAVAILABLE}, + }, nil +} + +func protoComponentsFromScanComponents(c *nodes.Components) *scannerV1.Components { + if c == nil { + return nil + } + + var namespace string + if c.OSNamespace == nil { + namespace = "unknown" + // TODO(ROX-14186): Also set a note here that this is an uncertified scan + } else { + namespace = c.OSNamespace.Name + } + + // For now, we only care about RHEL components, but this must be extended once we support non-RHCOS + var rhelComponents []*scannerV1.RHELComponent + var contentSets []string + if c.CertifiedRHELComponents != nil { + rhelComponents = convertAndDedupRHELComponents(c.CertifiedRHELComponents) + contentSets = c.CertifiedRHELComponents.ContentSets + } + + protoComponents := &scannerV1.Components{ + Namespace: namespace, + RhelComponents: rhelComponents, + RhelContentSets: contentSets, + } + return protoComponents +} + +func convertAndDedupRHELComponents(rc *database.RHELv2Components) []*scannerV1.RHELComponent { + if rc == nil || rc.Packages == nil { + log.Warn("No RHEL packages found in scan result") + return nil + } + + convertedComponents := make(map[string]*scannerV1.RHELComponent, 0) + for i, rhelc := range rc.Packages { + if rhelc == nil { + continue + } + comp := &scannerV1.RHELComponent{ + // The loop index is used as ID, as this field only needs to be unique for each NodeInventory result slice + Id: int64(i), + Name: rhelc.Name, + Namespace: rc.Dist, + Version: rhelc.Version, + Arch: rhelc.Arch, + Module: rhelc.Module, + Executables: nil, + } + if rhelc.Executables != nil { + comp.Executables = convertExecutables(rhelc.Executables) + } + compKey := makeComponentKey(comp) + if compKey != "" { + if _, contains := convertedComponents[compKey]; !contains { + log.Debugf("Adding component %v to convertedComponents", comp.Name) + convertedComponents[compKey] = comp + } else { + log.Warnf("Detected package collision in Node Inventory scan. Skipping package %s at index %d", compKey, i) + } + } + + } + return maps.Values(convertedComponents) +} + +func convertExecutables(exe []*scannerV1.Executable) []*scannerV1.Executable { + arr := make([]*scannerV1.Executable, len(exe)) + for i, executable := range exe { + arr[i] = &scannerV1.Executable{ + Path: executable.GetPath(), + RequiredFeatures: nil, + } + if executable.GetRequiredFeatures() != nil { + arr[i].RequiredFeatures = make([]*scannerV1.FeatureNameVersion, len(executable.GetRequiredFeatures())) + for i2, fnv := range executable.GetRequiredFeatures() { + arr[i].RequiredFeatures[i2] = &scannerV1.FeatureNameVersion{ + Name: fnv.GetName(), + Version: fnv.GetVersion(), + } + } + } + } + return arr +} + +func makeComponentKey(component *scannerV1.RHELComponent) string { + return component.Name + ":" + component.Version + ":" + component.Arch + ":" + component.Module +} diff --git a/pkg/nodeinventory/inventorizer_test.go b/pkg/nodeinventory/inventorizer_test.go new file mode 100644 index 000000000..651f5b823 --- /dev/null +++ b/pkg/nodeinventory/inventorizer_test.go @@ -0,0 +1,240 @@ +package nodeinventory + +import ( + "testing" + + "github.com/stackrox/scanner/database" + scannerV1 "github.com/stackrox/scanner/generated/scanner/api/v1" + "github.com/stretchr/testify/suite" +) + +func TestNodeInventorizer(t *testing.T) { + suite.Run(t, new(NodeInventorizerTestSuite)) +} + +type NodeInventorizerTestSuite struct { + suite.Suite +} + +func (s *NodeInventorizerTestSuite) TestConvertRHELComponentIDs() { + testCases := map[string]struct { + inComponents []*database.RHELv2Package + outComponents []*scannerV1.RHELComponent + expectedLen int + }{ + "nil-inComponents": { + inComponents: nil, + outComponents: make([]*scannerV1.RHELComponent, 0), + }, + "one-component": { + inComponents: []*database.RHELv2Package{ + { + Name: "zlib", + Version: "1.2.11-16.el8_2", + Arch: "x86_64", + ExecutableToDependencies: database.StringToStringsMap{ + "/usr/lib64/libz.so.1": {}, + "/usr/lib64/libz.so.1.2.11": {}, + }, + }, + }, + outComponents: []*scannerV1.RHELComponent{ + { + Id: 0, + Name: "zlib", + Namespace: "MockDist", + Version: "1.2.11-16.el8_2", + Arch: "x86_64", + }, + }, + expectedLen: 1, + }, + "multi-component": { + inComponents: []*database.RHELv2Package{ + { + Name: "zlib", + Version: "1.2.11-16.el8_2", + Arch: "x86_64", + ExecutableToDependencies: database.StringToStringsMap{ + "/usr/lib64/libz.so.1": {}, + "/usr/lib64/libz.so.1.2.11": {}, + }, + }, + { + Name: "redhat-release", + Version: "8.3-1.0.el8", + Arch: "x86_64", + }, + }, + outComponents: []*scannerV1.RHELComponent{ + { + Id: 0, + Name: "zlib", + Namespace: "MockDist", + Version: "1.2.11-16.el8_2", + Arch: "x86_64", + }, + { + Id: 1, + Name: "redhat-release", + Namespace: "MockDist", + Version: "8.3-1.0.el8", + Arch: "x86_64", + }, + }, + expectedLen: 2, + }, + "collision-component": { + inComponents: []*database.RHELv2Package{ + { + Name: "redhat-release", + Version: "8.3-1.0.el8", + Arch: "x86_64", + }, + { + Name: "redhat-release", + Version: "8.3-1.0.el8", + Arch: "x86_64", + }, + }, + outComponents: []*scannerV1.RHELComponent{ + { + Id: 0, + Name: "redhat-release", + Namespace: "MockDist", + Version: "8.3-1.0.el8", + Arch: "x86_64", + }, + }, + expectedLen: 1, + }, + } + for caseName, testCase := range testCases { + s.Run(caseName, func() { + mockComponents := &database.RHELv2Components{ + Dist: "MockDist", + CPEs: nil, + Packages: testCase.inComponents, + } + convertedComponents := convertAndDedupRHELComponents(mockComponents) + if testCase.inComponents != nil { + s.Equal(testCase.expectedLen, len(convertedComponents)) + s.ElementsMatch(testCase.outComponents, convertedComponents) + } else { + s.Nil(convertedComponents) + } + }) + } +} + +func (s *NodeInventorizerTestSuite) TestMakeComponentKey() { + testcases := map[string]struct { + component *scannerV1.RHELComponent + expected string + }{ + "Full component": { + component: &scannerV1.RHELComponent{ + Id: 0, + Name: "Name", + Version: "1.2.3", + Arch: "x42", + Module: "Mod", + }, + expected: "Name:1.2.3:x42:Mod", + }, + "Missing part": { + component: &scannerV1.RHELComponent{ + Id: 0, + Version: "1.2.3", + Arch: "x42", + Module: "Mod", + }, + expected: ":1.2.3:x42:Mod", + }, + "Internationalized": { + component: &scannerV1.RHELComponent{ + Id: 0, + Name: "日本語", + Version: "1.2.3", + Arch: "x42", + Module: "Mod", + }, + expected: "日本語:1.2.3:x42:Mod", + }, + } + + for testName, testCase := range testcases { + s.Run(testName, func() { + s.Equal(testCase.expected, makeComponentKey(testCase.component)) + }) + } +} + +func (s *NodeInventorizerTestSuite) TestConvertExecutable() { + testcases := map[string]struct { + exe []*scannerV1.Executable + expected []*scannerV1.Executable + }{ + "RequiredFeatures not empty": { + exe: []*scannerV1.Executable{ + { + Path: "/root/1", + RequiredFeatures: []*scannerV1.FeatureNameVersion{ + { + Name: "name1", + Version: "version1", + }, + }, + }, + }, + expected: []*scannerV1.Executable{ + { + Path: "/root/1", + RequiredFeatures: []*scannerV1.FeatureNameVersion{ + { + Name: "name1", + Version: "version1", + }, + }, + }, + }, + }, + "RequiredFeatures empty": { + exe: []*scannerV1.Executable{ + { + Path: "/root/1", + RequiredFeatures: []*scannerV1.FeatureNameVersion{}, + }, + }, + expected: []*scannerV1.Executable{ + { + Path: "/root/1", + RequiredFeatures: []*scannerV1.FeatureNameVersion{}, + }, + }, + }, + "RequiredFeatures nil": { + exe: []*scannerV1.Executable{ + { + Path: "/root/1", + RequiredFeatures: nil, + }, + }, + expected: []*scannerV1.Executable{ + { + Path: "/root/1", + RequiredFeatures: nil, + }, + }, + }, + } + + for testName, testCase := range testcases { + s.Run(testName, func() { + for i, got := range convertExecutables(testCase.exe) { + s.Equal(testCase.expected[i].GetPath(), got.GetPath()) + s.Equal(testCase.expected[i].GetRequiredFeatures(), got.GetRequiredFeatures()) + } + }) + } +} diff --git a/proto/scanner/api/v1/node_inventory_service.proto b/proto/scanner/api/v1/node_inventory_service.proto new file mode 100644 index 000000000..4bcee9410 --- /dev/null +++ b/proto/scanner/api/v1/node_inventory_service.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +option go_package = "scannerV1"; + +option java_package = "io.stackrox.proto.api.scanner.v1"; + +import weak "google/api/annotations.proto"; + +import "scanner/api/v1/note.proto"; +import "scanner/api/v1/component.proto"; + +package scannerV1; + +message GetNodeInventoryResponse { + string node_name = 1; + Components components = 2; + repeated Note notes = 3; +} + +message GetNodeInventoryRequest {} + +// NodeInventoryService is used in Secured Clusters to fetch information from Nodes and communicate with other +// Secured Cluster components, like the compliance container in Collector. +service NodeInventoryService { + rpc GetNodeInventory(GetNodeInventoryRequest) returns (GetNodeInventoryResponse) {} +}