Repository: netboxlabs/orb-discovery
Pull request #93: feat: adds basic default support (descriptions and tags)
Author: login: jajeffries
Labels: name: released
Created: 2025-05-21T19:41:12Z | Merged: 2025-05-22T15:22:22Z

This pull request introduces significant enhancements to the SNMP discovery service, including the addition of configuration options, support for entity-specific default values, and improvements to the mapping logic. It also includes updates to tests to reflect these changes. Below is a summary of the most important changes:

### Configuration Enhancements

* Added detailed documentation in `snmp-discovery/README.md` for configuring the SNMP discovery service using a YAML file. This includes sections for `defaults`, `mappings`, `authentication`, and `scope`. The `defaults` section allows specifying global and entity-specific default values for discovered entities.

### Code Enhancements

* Introduced the `EntityDefaults` struct in `snmp-discovery/config/config.go` to support entity-specific default values for `IPAddress`, `Interface`, and `Device` entities. These defaults include `description`, `comments`, and `tags`.
* Updated the `Map` methods in `snmp-discovery/mapping/mappers.go` for `IPAddressMapper`, `InterfaceMapper`, and `DeviceMapper` to apply global and entity-specific defaults during the mapping process. This ensures that discovered entities inherit appropriate default values if not explicitly provided. [[1]](diffhunk://#diff-480b988f8d1afe0ad0f2780a5b6bfa716acc21e684a110065226f573af92925eR50-R93) [[2]](diffhunk://#diff-480b988f8d1afe0ad0f2780a5b6bfa716acc21e684a110065226f573af92925eR125-R154) [[3]](diffhunk://#diff-480b988f8d1afe0ad0f2780a5b6bfa716acc21e684a110065226f573af92925eR163-R247)

### Refactoring and Simplification

* Replaced the use of `mappingEntry` with the updated `MappingEntry` type in `snmp-discovery/mapping/mappers.go` for consistency and improved readability.
* Added a `NewDeviceMapper` constructor in `snmp-discovery/mapping/mappers.go` to simplify the creation of `DeviceMapper` instances.

### Test Updates

* Refactored `snmp-discovery/mapping/mappers_test.go` to use the `mapping` package explicitly, ensuring consistency with the updated codebase. Updated test cases to reflect the new `MappingEntry` type and added test coverage for default value application. [[1]](diffhunk://#diff-683ef3f3451d82e76a861bc708a56f8b84a3cdaec883501f378724e5c3b93130L1-R1) [[2]](diffhunk://#diff-683ef3f3451d82e76a861bc708a56f8b84a3cdaec883501f378724e5c3b93130R13-R43) [[3]](diffhunk://#diff-683ef3f3451d82e76a861bc708a56f8b84a3cdaec883501f378724e5c3b93130L62-R83) [[4]](diffhunk://#diff-683ef3f3451d82e76a861bc708a56f8b84a3cdaec883501f378724e5c3b93130L279-R281) [[5]](diffhunk://#diff-683ef3f3451d82e76a861bc708a56f8b84a3cdaec883501f378724e5c3b93130L313-R345)


Diff:
diff --git a/snmp-discovery/README.md b/snmp-discovery/README.md
index f36c7310..a77c73ef 100644
--- a/snmp-discovery/README.md
+++ b/snmp-discovery/README.md
@@ -20,3 +20,100 @@ options:
   -a DIODE_APP_NAME_PREFIX, --diode-app-name-prefix DIODE_APP_NAME_PREFIX
                         Diode producer_app_name prefix
 ```
+
+## Configuration
+
+The SNMP discovery service is configured using a YAML file. The configuration file has the following structure:
+
+```yaml
+config:
+  schedule: "*/5 * * * *"  # Optional: Cron expression for scheduling
+  defaults:  # Optional: Default values for entities
+    description: "Global description"  # Optional: Global description for all entities
+    comments: "Global comments"  # Optional: Global comments for all entities
+    tags:  # Optional: Global tags for all entities
+      - "global"
+      - "snmp"
+    ip_address:  # Optional: Defaults specific to IP addresses
+      description: "IP Address description"
+      comments: "IP Address comments"
+      tags:
+        - "ip"
+        - "default"
+    interface:  # Optional: Defaults specific to interfaces
+      description: "Interface description"
+      tags:
+        - "interface"
+        - "default"
+    device:  # Optional: Defaults specific to devices
+      description: "Device description"
+      tags:
+        - "device"
+        - "default"
+scope:
+  targets:  # List of SNMP targets to discover
+    - host: "192.168.1.1"  # Required: Hostname or IP address
+      port: 161  # Optional: SNMP port (default: 161)
+  authentication:  # SNMP authentication settings
+    protocol_version: "SNMPv2c"  # Required: SNMP protocol version ("SNMPv1", "SNMPv2c", or "SNMPv3")
+    community: "public"  # Required for v1/v2c: SNMP community string
+    # Optional for v3:
+    # username: "user"
+    # security_level: authPriv # Allowed values: ("NoAuthNoPriv", "AuthNoPriv", "AuthPriv")
+    # auth_protocol: "SHA"
+    # auth_passphrase: "authkey"
+    # priv_protocol: "AES"
+    # priv_passphrase: "privkey"
+  retries: 3  # Optional: Number of SNMP retries (default: 0)
+```
+
+### Defaults
+
+The `defaults` section allows you to specify default values for entities discovered by SNMP. These defaults can be applied globally to all entities or specifically to certain entity types.
+
+#### Global Defaults
+
+Global defaults are applied to all entities if they don't have entity-specific defaults:
+
+- `description`: A global description for all entities
+- `comments`: Global comments for all entities
+- `tags`: Global tags for all entities
+
+#### Entity-Specific Defaults
+
+Entity-specific defaults override global defaults for their respective entity types:
+
+- `ipAddress`: Defaults for IP addresses
+  - `description`: Description for IP addresses
+  - `comments`: Comments for IP addresses
+  - `tags`: Tags for IP addresses
+
+- `interface`: Defaults for interfaces
+  - `description`: Description for interfaces
+  - `tags`: Tags for interfaces
+
+- `device`: Defaults for devices
+  - `description`: Description for devices
+  - `tags`: Tags for devices
+
+### Mappings
+
+The `mappings` section defines how SNMP OIDs are mapped to entities. Each mapping entry has the following fields:
+
+- `oid`: The SNMP OID to map
+- `entity`: The entity type to map to (e.g., "interface", "ipAddress", "device")
+- `field`: The field to map to
+- `identifierSize`: The size of the identifier in the OID (default: 1)
+- `mappingEntries`: Optional nested mappings for additional fields
+
+### Authentication
+
+The `authentication` section configures SNMP authentication settings:
+
+- `protocolVersion`: The SNMP protocol version ("1", "2c", or "3")
+- `community`: The SNMP community string (required for v1/v2c)
+- `username`: The SNMP username (required for v3)
+- `authProtocol`: The authentication protocol (required for v3)
+- `authKey`: The authentication key (required for v3)
+- `privProtocol`: The privacy protocol (required for v3)
+- `privKey`: The privacy key (required for v3)
diff --git a/snmp-discovery/config/config.go b/snmp-discovery/config/config.go
index 3dcd07ac..ca5f4c70 100644
--- a/snmp-discovery/config/config.go
+++ b/snmp-discovery/config/config.go
@@ -36,13 +36,23 @@ type Authentication struct {
 	PrivPassphrase  string `yaml:"priv_passphrase"`
 }
 
-// Defaults represents the supported default values for a policy
-type Defaults struct {
+// EntityDefaults represents default values for a specific entity type
+type EntityDefaults struct {
 	Description string   `yaml:"description,omitempty"`
 	Comments    string   `yaml:"comments,omitempty"`
 	Tags        []string `yaml:"tags,omitempty"`
 }
 
+// Defaults represents the supported default values for a policy
+type Defaults struct {
+	Description string         `yaml:"description,omitempty"`
+	Comments    string         `yaml:"comments,omitempty"`
+	Tags        []string       `yaml:"tags,omitempty"`
+	IPAddress   EntityDefaults `yaml:"ip_address,omitempty"`
+	Interface   EntityDefaults `yaml:"interface,omitempty"`
+	Device      EntityDefaults `yaml:"device,omitempty"`
+}
+
 // PolicyConfig represents the configuration of a policy
 type PolicyConfig struct {
 	Schedule    *string  `yaml:"schedule,omitempty"`
diff --git a/snmp-discovery/mapping/mappers.go b/snmp-discovery/mapping/mappers.go
index 091369b7..b82908db 100644
--- a/snmp-discovery/mapping/mappers.go
+++ b/snmp-discovery/mapping/mappers.go
@@ -16,10 +16,11 @@ import (
 type IPAddressMapper struct{}
 
 // Map maps IP addresses to entities
-func (m *IPAddressMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEntry *mappingEntry, entityRegistry *EntityRegistry, logger *slog.Logger) diode.Entity {
+func (m *IPAddressMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEntry *Entry, entityRegistry *EntityRegistry, logger *slog.Logger) diode.Entity {
 	logger.Debug("Mapping values to ipAddress entity", "values", values, "mappingEntry", mappingEntry)
 	ipAddress := diode.IPAddress{}
 
+	fieldFound := false
 	// for each value in the map, map it to the ip address entity
 	for objectID, value := range values {
 		logger.Debug("Mapping value to ipAddress entity", "objectID", objectID, "value", value)
@@ -29,6 +30,7 @@ func (m *IPAddressMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEn
 				case "address":
 					x := fmt.Sprintf("%s/32", string(value.Index))
 					ipAddress.Address = &x
+					fieldFound = true
 				case "assigned_object":
 					if propertyMappingEntry.Relationship != (config.Relationship{}) {
 						linkedEntity := entityRegistry.GetOrCreateEntity(EntityType(propertyMappingEntry.Relationship.Type), ObjectIDIndex(value.Value))
@@ -39,6 +41,7 @@ func (m *IPAddressMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEn
 						// Handle relationship mapping
 						if propertyMappingEntry.Relationship.Type == "interface" {
 							ipAddress.AssignedObject = linkedEntity.(*diode.Interface)
+							fieldFound = true
 						}
 					}
 				default:
@@ -47,6 +50,42 @@ func (m *IPAddressMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEn
 			}
 		}
 	}
+
+	// Apply defaults if available
+	if defaults := entityRegistry.GetDefaults(); defaults != nil && fieldFound {
+		// Apply IP address specific defaults
+		if defaults.IPAddress.Description != "" {
+			ipAddress.Description = &defaults.IPAddress.Description
+		}
+		if defaults.IPAddress.Comments != "" {
+			ipAddress.Comments = &defaults.IPAddress.Comments
+		}
+		var tags []*diode.Tag
+		// Add entity-specific tags
+		if len(defaults.IPAddress.Tags) > 0 {
+			for _, tag := range defaults.IPAddress.Tags {
+				tags = append(tags, &diode.Tag{Name: &tag})
+			}
+		}
+		// Add global tags
+		if len(defaults.Tags) > 0 {
+			for _, tag := range defaults.Tags {
+				tags = append(tags, &diode.Tag{Name: &tag})
+			}
+		}
+		if len(tags) > 0 {
+			ipAddress.Tags = tags
+		}
+
+		// Apply global defaults if not overridden by entity-specific defaults
+		if ipAddress.Description == nil && defaults.Description != "" {
+			ipAddress.Description = &defaults.Description
+		}
+		if ipAddress.Comments == nil && defaults.Comments != "" {
+			ipAddress.Comments = &defaults.Comments
+		}
+	}
+
 	return &ipAddress
 }
 
@@ -54,10 +93,11 @@ func (m *IPAddressMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEn
 type InterfaceMapper struct{}
 
 // Map maps interfaces to entities
-func (m *InterfaceMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEntry *mappingEntry, entityRegistry *EntityRegistry, logger *slog.Logger) diode.Entity {
+func (m *InterfaceMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEntry *Entry, entityRegistry *EntityRegistry, logger *slog.Logger) diode.Entity {
 	logger.Debug("Mapping values to interface entity", "values", values, "mappingEntry", mappingEntry)
 	interfaceEntity := entityRegistry.GetOrCreateEntity(EntityType(mappingEntry.Entity), getIndex(values)).(*diode.Interface)
 
+	fieldFound := false
 	for objectID, value := range values {
 		for _, propertyMappingEntry := range mappingEntry.MappingEntries {
 			if objectID.HasParent(propertyMappingEntry.OID) {
@@ -65,6 +105,7 @@ func (m *InterfaceMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEn
 				switch propertyMappingEntry.Field {
 				case "name":
 					interfaceEntity.Name = &value.Value
+					fieldFound = true
 				case "speed":
 					speed, err := strconv.Atoi(value.Value)
 					if err != nil {
@@ -73,19 +114,52 @@ func (m *InterfaceMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEn
 					}
 					speed64 := int64(speed)
 					interfaceEntity.Speed = &speed64
+					fieldFound = true
 				case "macAddress":
 					interfaceEntity.PrimaryMacAddress = &diode.MACAddress{
 						MacAddress: &value.Value,
 					}
+					fieldFound = true
 				case "adminStatus":
 					enabled := value.Value == "1"
 					interfaceEntity.Enabled = &enabled
+					fieldFound = true
 				default:
 					logger.Warn("Unknown field", "field", propertyMappingEntry.Field)
 				}
 			}
 		}
 	}
+
+	// Apply defaults if available
+	if defaults := entityRegistry.GetDefaults(); defaults != nil && fieldFound {
+		// Apply interface specific defaults
+		if defaults.Interface.Description != "" {
+			interfaceEntity.Description = &defaults.Interface.Description
+		}
+		var tags []*diode.Tag
+		// Add entity-specific tags
+		if len(defaults.Interface.Tags) > 0 {
+			for _, tag := range defaults.Interface.Tags {
+				tags = append(tags, &diode.Tag{Name: &tag})
+			}
+		}
+		// Add global tags
+		if len(defaults.Tags) > 0 {
+			for _, tag := range defaults.Tags {
+				tags = append(tags, &diode.Tag{Name: &tag})
+			}
+		}
+		if len(tags) > 0 {
+			interfaceEntity.Tags = tags
+		}
+
+		// Apply global defaults if not overridden by entity-specific defaults
+		if interfaceEntity.Description == nil && defaults.Description != "" {
+			interfaceEntity.Description = &defaults.Description
+		}
+	}
+
 	return interfaceEntity
 }
 
@@ -94,52 +168,94 @@ type DeviceMapper struct {
 	devices data.DeviceDataRetreiver
 }
 
+// NewDeviceMapper creates a new DeviceMapper
+func NewDeviceMapper(devices data.DeviceDataRetreiver) *DeviceMapper {
+	return &DeviceMapper{
+		devices: devices,
+	}
+}
+
 // Map maps devices to entities
-func (m *DeviceMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEntry *mappingEntry, _ *EntityRegistry, logger *slog.Logger) diode.Entity {
+func (m *DeviceMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEntry *Entry, entityRegistry *EntityRegistry, logger *slog.Logger) diode.Entity {
 	logger.Debug("Mapping values to device entity", "values", values, "mappingEntry", mappingEntry)
-	device := diode.Device{}
+	deviceEntity := entityRegistry.GetOrCreateEntity(EntityType(mappingEntry.Entity), getIndex(values)).(*diode.Device)
 
+	fieldFound := false
 	for objectID, value := range values {
 		for _, propertyMappingEntry := range mappingEntry.MappingEntries {
 			if objectID.HasParent(propertyMappingEntry.OID) {
 				logger.Debug("Mapping value to device entity with mapper", "objectID", objectID, "value", value, "mappingEntry", propertyMappingEntry)
 				switch propertyMappingEntry.Field {
 				case "name":
-					device.Name = &value.Value
+					deviceEntity.Name = &value.Value
+					fieldFound = true
 				case "platform":
+					// Use getDeviceIDs to get the manufacturer and model
 					manufacturerID, modelID, err := m.getDeviceIDs(value.Value)
 					if err != nil {
-						logger.Warn("Error getting device IDs, skipping", "error", err, "objectID", objectID, "value", value.Value)
+						logger.Warn("Error getting device IDs", "error", err, "value", value.Value)
 						continue
 					}
 					manufacturer, err := m.devices.GetManufacturer(manufacturerID)
 					if err != nil {
-						logger.Warn("Error getting manufacturer, skipping", "error", err, "objectID", objectID, "value", value.Value)
+						logger.Warn("Error getting manufacturer", "error", err, "manufacturerID", manufacturerID)
 						continue
 					}
-					device.Platform = &diode.Platform{
-						Manufacturer: &diode.Manufacturer{
-							Name: diode.String(manufacturer),
-						},
+
+					manufacturerEntity := diode.Manufacturer{
+						Name: &manufacturer,
 					}
-					model, err := m.devices.GetDeviceModel(modelID)
+
+					deviceEntity.Platform = &diode.Platform{
+						Manufacturer: &manufacturerEntity,
+					}
+
+					deviceModel, err := m.devices.GetDeviceModel(modelID)
 					if err != nil {
-						logger.Warn("Error getting device model, assigning default model", "error", err, "objectID", objectID, "value", value.Value)
-						continue
+						logger.Warn("Error getting device model", "error", err, "modelID", modelID)
 					}
-					device.DeviceType = &diode.DeviceType{
-						Manufacturer: &diode.Manufacturer{
-							Name: diode.String(manufacturer),
-						},
-						Model: &model,
+					deviceEntity.DeviceType = &diode.DeviceType{
+						Model:        &deviceModel,
+						Manufacturer: &manufacturerEntity,
 					}
+					fieldFound = true
 				default:
 					logger.Warn("Unknown field", "field", propertyMappingEntry.Field)
 				}
 			}
 		}
 	}
-	return &device
+
+	// Apply defaults if available
+	if defaults := entityRegistry.GetDefaults(); fieldFound && defaults != nil {
+		// Apply device specific defaults
+		if defaults.Device.Description != "" {
+			deviceEntity.Description = &defaults.Device.Description
+		}
+		var tags []*diode.Tag
+		// Add entity-specific tags
+		if len(defaults.Device.Tags) > 0 {
+			for _, tag := range defaults.Device.Tags {
+				tags = append(tags, &diode.Tag{Name: &tag})
+			}
+		}
+		// Add global tags
+		if len(defaults.Tags) > 0 {
+			for _, tag := range defaults.Tags {
+				tags = append(tags, &diode.Tag{Name: &tag})
+			}
+		}
+		if len(tags) > 0 {
+			deviceEntity.Tags = tags
+		}
+
+		// Apply global defaults if not overridden by entity-specific defaults
+		if deviceEntity.Description == nil && defaults.Description != "" {
+			deviceEntity.Description = &defaults.Description
+		}
+	}
+
+	return deviceEntity
 }
 
 func (m *DeviceMapper) getDeviceIDs(objectID string) (int, int, error) {
diff --git a/snmp-discovery/mapping/mappers_test.go b/snmp-discovery/mapping/mappers_test.go
index 9f8072d9..3e566e6b 100644
--- a/snmp-discovery/mapping/mappers_test.go
+++ b/snmp-discovery/mapping/mappers_test.go
@@ -1,4 +1,4 @@
-package mapping
+package mapping_test
 
 import (
 	"fmt"
@@ -10,36 +10,36 @@ import (
 	"github.com/stretchr/testify/mock"
 
 	"github.com/netboxlabs/orb-discovery/snmp-discovery/config"
+	"github.com/netboxlabs/orb-discovery/snmp-discovery/mapping"
 )
 
 func TestIPAddressMapper_Map(t *testing.T) {
 	logger := slog.Default()
-	registry := NewEntityRegistry(logger)
-	mapper := &IPAddressMapper{}
 
 	tests := []struct {
 		name           string
-		values         map[ObjectIDIndex]*ObjectIDValue
-		mappingEntry   *mappingEntry
+		values         map[mapping.ObjectIDIndex]*mapping.ObjectIDValue
+		mappingEntry   *mapping.Entry
+		defaults       *config.Defaults
 		expectedEntity *diode.IPAddress
 		expectError    bool
 	}{
 		{
 			name: "successful mapping with all fields",
-			values: map[ObjectIDIndex]*ObjectIDValue{
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
 				"1.3.6.1.2.1.4.20.1.1.192.168.1.1": {
 					OID:    "1.3.6.1.2.1.4.20.1.1.192.168.1.1",
 					Index:  "192.168.1.1",
 					Parent: "1.3.6.1.2.1.4.20.1.1",
 					Value:  "192.168.1.1",
-					Type:   IPAddress,
+					Type:   mapping.IPAddress,
 				},
 			},
-			mappingEntry: &mappingEntry{
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.4.20.1.1",
 				Entity: "ipAddress",
 				Field:  "_id",
-				MappingEntries: []mappingEntry{
+				MappingEntries: []mapping.Entry{
 					{
 						OID:    "1.3.6.1.2.1.4.20.1.1",
 						Entity: "ipAddress",
@@ -52,34 +52,170 @@ func TestIPAddressMapper_Map(t *testing.T) {
 					},
 				},
 			},
+			defaults: nil,
 			expectedEntity: &diode.IPAddress{
 				Address: stringPtr("192.168.1.1/32"),
 			},
 			expectError: false,
 		},
+		{
+			name: "mapping with global defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.4.20.1.1.192.168.1.1": {
+					OID:    "1.3.6.1.2.1.4.20.1.1.192.168.1.1",
+					Index:  "192.168.1.1",
+					Parent: "1.3.6.1.2.1.4.20.1.1",
+					Value:  "192.168.1.1",
+					Type:   mapping.IPAddress,
+				},
+			},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.4.20.1.1",
+				Entity: "ipAddress",
+				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.4.20.1.1",
+						Entity: "ipAddress",
+						Field:  "_id",
+					},
+					{
+						OID:    "1.3.6.1.2.1.4.20.1.1",
+						Entity: "ipAddress",
+						Field:  "address",
+					},
+				},
+			},
+			defaults: &config.Defaults{
+				Description: "Global description",
+				Tags:        []string{"global-tag1", "global-tag2"},
+			},
+			expectedEntity: &diode.IPAddress{
+				Address:     stringPtr("192.168.1.1/32"),
+				Description: stringPtr("Global description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("global-tag1")},
+					{Name: stringPtr("global-tag2")},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name: "mapping with entity-specific defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.4.20.1.1.192.168.1.1": {
+					OID:    "1.3.6.1.2.1.4.20.1.1.192.168.1.1",
+					Index:  "192.168.1.1",
+					Parent: "1.3.6.1.2.1.4.20.1.1",
+					Value:  "192.168.1.1",
+					Type:   mapping.IPAddress,
+				},
+			},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.4.20.1.1",
+				Entity: "ipAddress",
+				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.4.20.1.1",
+						Entity: "ipAddress",
+						Field:  "_id",
+					},
+					{
+						OID:    "1.3.6.1.2.1.4.20.1.1",
+						Entity: "ipAddress",
+						Field:  "address",
+					},
+				},
+			},
+			defaults: &config.Defaults{
+				IPAddress: config.EntityDefaults{
+					Description: "IP Address specific description",
+					Tags:        []string{"ip-tag1", "ip-tag2"},
+				},
+			},
+			expectedEntity: &diode.IPAddress{
+				Address:     stringPtr("192.168.1.1/32"),
+				Description: stringPtr("IP Address specific description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("ip-tag1")},
+					{Name: stringPtr("ip-tag2")},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name: "mapping with both global and entity-specific defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.4.20.1.1.192.168.1.1": {
+					OID:    "1.3.6.1.2.1.4.20.1.1.192.168.1.1",
+					Index:  "192.168.1.1",
+					Parent: "1.3.6.1.2.1.4.20.1.1",
+					Value:  "192.168.1.1",
+					Type:   mapping.IPAddress,
+				},
+			},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.4.20.1.1",
+				Entity: "ipAddress",
+				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.4.20.1.1",
+						Entity: "ipAddress",
+						Field:  "_id",
+					},
+					{
+						OID:    "1.3.6.1.2.1.4.20.1.1",
+						Entity: "ipAddress",
+						Field:  "address",
+					},
+				},
+			},
+			defaults: &config.Defaults{
+				Description: "Global description",
+				Tags:        []string{"global-tag1", "global-tag2"},
+				IPAddress: config.EntityDefaults{
+					Description: "IP Address specific description",
+					Tags:        []string{"ip-tag1", "ip-tag2"},
+				},
+			},
+			expectedEntity: &diode.IPAddress{
+				Address:     stringPtr("192.168.1.1/32"),
+				Description: stringPtr("IP Address specific description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("ip-tag1")},
+					{Name: stringPtr("ip-tag2")},
+					{Name: stringPtr("global-tag1")},
+					{Name: stringPtr("global-tag2")},
+				},
+			},
+			expectError: false,
+		},
 		{
 			name: "mapping with interface relationship",
-			values: map[ObjectIDIndex]*ObjectIDValue{
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
 				"1.3.6.1.2.1.4.20.1.1.192.168.1.1": {
 					OID:    "1.3.6.1.2.1.4.20.1.1.192.168.1.1",
 					Index:  "192.168.1.1",
 					Parent: "1.3.6.1.2.1.4.20.1.1",
 					Value:  "192.168.1.1",
-					Type:   IPAddress,
+					Type:   mapping.IPAddress,
 				},
 				"1.3.6.1.2.1.4.20.1.2.192.168.1.1": {
 					OID:    "1.3.6.1.2.1.4.20.1.2.192.168.1.1",
 					Index:  "192.168.1.1",
 					Parent: "1.3.6.1.2.1.4.20.1.2",
 					Value:  "1",
-					Type:   Integer,
+					Type:   mapping.Integer,
 				},
 			},
-			mappingEntry: &mappingEntry{
+			defaults: nil,
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.4.20.1.1",
 				Entity: "ipAddress",
 				Field:  "_id",
-				MappingEntries: []mappingEntry{
+				MappingEntries: []mapping.Entry{
 					{
 						OID:    "1.3.6.1.2.1.4.20.1.1",
 						Entity: "ipAddress",
@@ -107,8 +243,8 @@ func TestIPAddressMapper_Map(t *testing.T) {
 		},
 		{
 			name:   "empty values map",
-			values: map[ObjectIDIndex]*ObjectIDValue{},
-			mappingEntry: &mappingEntry{
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{},
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.4.20.1.1",
 				Entity: "ipAddress",
 				Field:  "_id",
@@ -120,6 +256,9 @@ func TestIPAddressMapper_Map(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			registry := mapping.NewEntityRegistry(logger)
+			mapper := &mapping.IPAddressMapper{}
+			registry.SetDefaults(tt.defaults)
 			entity := mapper.Map(tt.values, tt.mappingEntry, registry, logger)
 
 			if tt.expectError {
@@ -131,6 +270,13 @@ func TestIPAddressMapper_Map(t *testing.T) {
 			ipAddress, ok := entity.(*diode.IPAddress)
 			assert.True(t, ok)
 			assert.Equal(t, tt.expectedEntity.Address, ipAddress.Address)
+			assert.Equal(t, tt.expectedEntity.Description, ipAddress.Description)
+			if tt.expectedEntity.Tags != nil {
+				assert.Equal(t, len(tt.expectedEntity.Tags), len(ipAddress.Tags))
+				for i, tag := range tt.expectedEntity.Tags {
+					assert.Equal(t, tag.Name, ipAddress.Tags[i].Name)
+				}
+			}
 		})
 	}
 }
@@ -138,55 +284,56 @@ func TestIPAddressMapper_Map(t *testing.T) {
 func TestInterfaceMapper_Map(t *testing.T) {
 	tests := []struct {
 		name           string
-		values         map[ObjectIDIndex]*ObjectIDValue
-		mappingEntry   *mappingEntry
+		values         map[mapping.ObjectIDIndex]*mapping.ObjectIDValue
+		mappingEntry   *mapping.Entry
+		defaults       *config.Defaults
 		expectedEntity *diode.Interface
 		expectError    bool
 	}{
 		{
 			name: "successful mapping with all fields",
-			values: map[ObjectIDIndex]*ObjectIDValue{
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
 				"1.3.6.1.2.1.2.2.1.1.1": {
 					OID:    "1.3.6.1.2.1.2.2.1.1.1",
 					Index:  "1",
 					Parent: "1.3.6.1.2.1.2.2.1.1",
 					Value:  "1",
-					Type:   Integer,
+					Type:   mapping.Integer,
 				},
 				"1.3.6.1.2.1.2.2.1.2.1": {
 					OID:    "1.3.6.1.2.1.2.2.1.2.1",
 					Index:  "1",
 					Parent: "1.3.6.1.2.1.2.2.1.2",
 					Value:  "eth0",
-					Type:   OctetString,
+					Type:   mapping.OctetString,
 				},
 				"1.3.6.1.2.1.2.2.1.5.1": {
 					OID:    "1.3.6.1.2.1.2.2.1.5.1",
 					Index:  "1",
 					Parent: "1.3.6.1.2.1.2.2.1.5",
 					Value:  "1000000",
-					Type:   Integer,
+					Type:   mapping.Integer,
 				},
 				"1.3.6.1.2.1.2.2.1.6.1": {
 					OID:    "1.3.6.1.2.1.2.2.1.6.1",
 					Index:  "1",
 					Parent: "1.3.6.1.2.1.2.2.1.6",
 					Value:  "00:11:22:33:44:55",
-					Type:   OctetString,
+					Type:   mapping.OctetString,
 				},
 				"1.3.6.1.2.1.2.2.1.7.1": {
 					OID:    "1.3.6.1.2.1.2.2.1.7.1",
 					Index:  "1",
 					Parent: "1.3.6.1.2.1.2.2.1.7",
 					Value:  "1",
-					Type:   Integer,
+					Type:   mapping.Integer,
 				},
 			},
-			mappingEntry: &mappingEntry{
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.2.2.1.1",
 				Entity: "interface",
 				Field:  "_id",
-				MappingEntries: []mappingEntry{
+				MappingEntries: []mapping.Entry{
 					{
 						OID:    "1.3.6.1.2.1.2.2.1.1",
 						Entity: "interface",
@@ -214,6 +361,7 @@ func TestInterfaceMapper_Map(t *testing.T) {
 					},
 				},
 			},
+			defaults: nil,
 			expectedEntity: &diode.Interface{
 				Name:              stringPtr("eth0"),
 				Speed:             int64Ptr(1000000),
@@ -222,29 +370,184 @@ func TestInterfaceMapper_Map(t *testing.T) {
 			},
 			expectError: false,
 		},
+		{
+			name: "mapping with global defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.2.2.1.1.1": {
+					OID:    "1.3.6.1.2.1.2.2.1.1.1",
+					Index:  "1",
+					Parent: "1.3.6.1.2.1.2.2.1.1",
+					Value:  "1",
+					Type:   mapping.Integer,
+				},
+				"1.3.6.1.2.1.2.2.1.2.1": {
+					OID:    "1.3.6.1.2.1.2.2.1.2.1",
+					Index:  "1",
+					Parent: "1.3.6.1.2.1.2.2.1.2",
+					Value:  "eth0",
+					Type:   mapping.OctetString,
+				},
+			},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.2.2.1.1",
+				Entity: "interface",
+				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.2.2.1.1",
+						Entity: "interface",
+						Field:  "_id",
+					},
+					{
+						OID:    "1.3.6.1.2.1.2.2.1.2",
+						Entity: "interface",
+						Field:  "name",
+					},
+				},
+			},
+			defaults: &config.Defaults{
+				Description: "Global description",
+				Tags:        []string{"global-tag1", "global-tag2"},
+			},
+			expectedEntity: &diode.Interface{
+				Name:        stringPtr("eth0"),
+				Description: stringPtr("Global description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("global-tag1")},
+					{Name: stringPtr("global-tag2")},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name: "mapping with entity-specific defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.2.2.1.1.1": {
+					OID:    "1.3.6.1.2.1.2.2.1.1.1",
+					Index:  "1",
+					Parent: "1.3.6.1.2.1.2.2.1.1",
+					Value:  "1",
+					Type:   mapping.Integer,
+				},
+				"1.3.6.1.2.1.2.2.1.2.1": {
+					OID:    "1.3.6.1.2.1.2.2.1.2.1",
+					Index:  "1",
+					Parent: "1.3.6.1.2.1.2.2.1.2",
+					Value:  "eth0",
+					Type:   mapping.OctetString,
+				},
+			},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.2.2.1.1",
+				Entity: "interface",
+				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.2.2.1.1",
+						Entity: "interface",
+						Field:  "_id",
+					},
+					{
+						OID:    "1.3.6.1.2.1.2.2.1.2",
+						Entity: "interface",
+						Field:  "name",
+					},
+				},
+			},
+			defaults: &config.Defaults{
+				Interface: config.EntityDefaults{
+					Description: "Interface specific description",
+					Tags:        []string{"interface-tag1", "interface-tag2"},
+				},
+			},
+			expectedEntity: &diode.Interface{
+				Name:        stringPtr("eth0"),
+				Description: stringPtr("Interface specific description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("interface-tag1")},
+					{Name: stringPtr("interface-tag2")},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name: "mapping with both global and entity-specific defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.2.2.1.1.1": {
+					OID:    "1.3.6.1.2.1.2.2.1.1.1",
+					Index:  "1",
+					Parent: "1.3.6.1.2.1.2.2.1.1",
+					Value:  "1",
+					Type:   mapping.Integer,
+				},
+				"1.3.6.1.2.1.2.2.1.2.1": {
+					OID:    "1.3.6.1.2.1.2.2.1.2.1",
+					Index:  "1",
+					Parent: "1.3.6.1.2.1.2.2.1.2",
+					Value:  "eth0",
+					Type:   mapping.OctetString,
+				},
+			},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.2.2.1.1",
+				Entity: "interface",
+				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.2.2.1.1",
+						Entity: "interface",
+						Field:  "_id",
+					},
+					{
+						OID:    "1.3.6.1.2.1.2.2.1.2",
+						Entity: "interface",
+						Field:  "name",
+					},
+				},
+			},
+			defaults: &config.Defaults{
+				Description: "Global description",
+				Tags:        []string{"global-tag1", "global-tag2"},
+				Interface: config.EntityDefaults{
+					Description: "Interface specific description",
+					Tags:        []string{"interface-tag1", "interface-tag2"},
+				},
+			},
+			expectedEntity: &diode.Interface{
+				Name:        stringPtr("eth0"),
+				Description: stringPtr("Interface specific description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("interface-tag1")},
+					{Name: stringPtr("interface-tag2")},
+					{Name: stringPtr("global-tag1")},
+					{Name: stringPtr("global-tag2")},
+				},
+			},
+			expectError: false,
+		},
 		{
 			name: "mapping with invalid speed value",
-			values: map[ObjectIDIndex]*ObjectIDValue{
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
 				"1.3.6.1.2.1.2.2.1.1.1": {
 					OID:    "1.3.6.1.2.1.2.2.1.1.1",
 					Index:  "1",
 					Parent: "1.3.6.1.2.1.2.2.1.1",
 					Value:  "1",
-					Type:   Integer,
+					Type:   mapping.Integer,
 				},
 				"1.3.6.1.2.1.2.2.1.5.1": {
 					OID:    "1.3.6.1.2.1.2.2.1.5.1",
 					Index:  "1",
 					Parent: "1.3.6.1.2.1.2.2.1.5",
 					Value:  "invalid",
-					Type:   Integer,
+					Type:   mapping.Integer,
 				},
 			},
-			mappingEntry: &mappingEntry{
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.2.2.1.1",
 				Entity: "interface",
 				Field:  "_id",
-				MappingEntries: []mappingEntry{
+				MappingEntries: []mapping.Entry{
 					{
 						OID:    "1.3.6.1.2.1.2.2.1.1",
 						Entity: "interface",
@@ -262,8 +565,8 @@ func TestInterfaceMapper_Map(t *testing.T) {
 		},
 		{
 			name:   "empty values map",
-			values: map[ObjectIDIndex]*ObjectIDValue{},
-			mappingEntry: &mappingEntry{
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{},
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.2.2.1.1",
 				Entity: "interface",
 				Field:  "_id",
@@ -276,8 +579,11 @@ func TestInterfaceMapper_Map(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			logger := slog.Default()
-			registry := NewEntityRegistry(logger)
-			mapper := &InterfaceMapper{}
+			registry := mapping.NewEntityRegistry(logger)
+			if tt.defaults != nil {
+				registry.SetDefaults(tt.defaults)
+			}
+			mapper := &mapping.InterfaceMapper{}
 			entity := mapper.Map(tt.values, tt.mappingEntry, registry, logger)
 
 			if tt.expectError {
@@ -294,13 +600,19 @@ func TestInterfaceMapper_Map(t *testing.T) {
 				assert.Equal(t, tt.expectedEntity.PrimaryMacAddress.MacAddress, iface.PrimaryMacAddress.MacAddress)
 			}
 			assert.Equal(t, tt.expectedEntity.Enabled, iface.Enabled)
+			assert.Equal(t, tt.expectedEntity.Description, iface.Description)
+			if tt.expectedEntity.Tags != nil {
+				assert.Equal(t, len(tt.expectedEntity.Tags), len(iface.Tags))
+				for i, tag := range tt.expectedEntity.Tags {
+					assert.Equal(t, tag.Name, iface.Tags[i].Name)
+				}
+			}
 		})
 	}
 }
 
 func TestDeviceMapper_Map(t *testing.T) {
 	logger := slog.Default()
-	registry := NewEntityRegistry(logger)
 
 	// Create a mock manufacturer data retriever
 	mockManufacturers := &MockManufacturerDataRetriever{}
@@ -310,40 +622,39 @@ func TestDeviceMapper_Map(t *testing.T) {
 	mockManufacturers.On("GetDeviceModel", 1234).Return("cisco4000", nil)
 	mockManufacturers.On("GetDeviceModel", 999).Return("", fmt.Errorf("device model not found"))
 
-	mapper := &DeviceMapper{
-		devices: mockManufacturers,
-	}
+	mapper := mapping.NewDeviceMapper(mockManufacturers)
 
 	tests := []struct {
 		name           string
-		values         map[ObjectIDIndex]*ObjectIDValue
-		mappingEntry   *mappingEntry
+		values         map[mapping.ObjectIDIndex]*mapping.ObjectIDValue
+		mappingEntry   *mapping.Entry
+		defaults       *config.Defaults
 		expectedEntity *diode.Device
 		expectError    bool
 	}{
 		{
 			name: "successful mapping with name and platform",
-			values: map[ObjectIDIndex]*ObjectIDValue{
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
 				"1.3.6.1.2.1.1.5.0": {
 					OID:    "1.3.6.1.2.1.1.5.0",
 					Index:  "0",
 					Parent: "1.3.6.1.2.1.1.5",
 					Value:  "router1",
-					Type:   OctetString,
+					Type:   mapping.OctetString,
 				},
 				"1.3.6.1.2.1.1.2.0": {
 					OID:    "1.3.6.1.2.1.1.2.0",
 					Index:  "0",
 					Parent: "1.3.6.1.2.1.1.2",
 					Value:  "1.3.6.1.4.1.9.1.1234",
-					Type:   ObjectIdentifier,
+					Type:   mapping.ObjectIdentifier,
 				},
 			},
-			mappingEntry: &mappingEntry{
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.1",
 				Entity: "device",
 				Field:  "_id",
-				MappingEntries: []mappingEntry{
+				MappingEntries: []mapping.Entry{
 					{
 						OID:    "1.3.6.1.2.1.1.5",
 						Entity: "device",
@@ -356,81 +667,171 @@ func TestDeviceMapper_Map(t *testing.T) {
 					},
 				},
 			},
+			defaults: nil,
 			expectedEntity: &diode.Device{
 				Name: stringPtr("router1"),
 				DeviceType: &diode.DeviceType{
 					Manufacturer: &diode.Manufacturer{
-						Name: diode.String("Cisco"),
+						Name: stringPtr("Cisco"),
 					},
-					Model: diode.String("cisco4000"),
+					Model: stringPtr("cisco4000"),
 				},
 				Platform: &diode.Platform{
 					Manufacturer: &diode.Manufacturer{
-						Name: diode.String("Cisco"),
+						Name: stringPtr("Cisco"),
 					},
 				},
 			},
 			expectError: false,
 		},
 		{
-			name: "mapping with unknown manufacturer",
-			values: map[ObjectIDIndex]*ObjectIDValue{
-				"1.3.6.1.2.1.1.2.0": {
-					OID:    "1.3.6.1.2.1.1.2.0",
+			name: "mapping with global defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.1.5.0": {
+					OID:    "1.3.6.1.2.1.1.5.0",
 					Index:  "0",
-					Parent: "1.3.6.1.2.1.1.2",
-					Value:  "1.3.6.1.4.1.999.1.1234",
-					Type:   ObjectIdentifier,
+					Parent: "1.3.6.1.2.1.1.5",
+					Value:  "router1",
+					Type:   mapping.OctetString,
 				},
 			},
-			mappingEntry: &mappingEntry{
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.1",
 				Entity: "device",
 				Field:  "_id",
-				MappingEntries: []mappingEntry{
+				MappingEntries: []mapping.Entry{
 					{
-						OID:    "1.3.6.1.2.1.1.2",
+						OID:    "1.3.6.1.2.1.1.5",
 						Entity: "device",
-						Field:  "platform",
+						Field:  "name",
 					},
 				},
 			},
+			defaults: &config.Defaults{
+				Description: "Global description",
+				Tags:        []string{"global-tag1", "global-tag2"},
+			},
 			expectedEntity: &diode.Device{
-				Platform: &diode.Platform{
-					Manufacturer: &diode.Manufacturer{
-						Name: diode.String("unknown"),
+				Name:        stringPtr("router1"),
+				Description: stringPtr("Global description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("global-tag1")},
+					{Name: stringPtr("global-tag2")},
+				},
+			},
+			expectError: false,
+		},
+		{
+			name: "mapping with entity-specific defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.1.5.0": {
+					OID:    "1.3.6.1.2.1.1.5.0",
+					Index:  "0",
+					Parent: "1.3.6.1.2.1.1.5",
+					Value:  "router1",
+					Type:   mapping.OctetString,
+				},
+			},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.1",
+				Entity: "device",
+				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.1.5",
+						Entity: "device",
+						Field:  "name",
 					},
 				},
 			},
+			defaults: &config.Defaults{
+				Device: config.EntityDefaults{
+					Description: "Device specific description",
+					Tags:        []string{"device-tag1", "device-tag2"},
+				},
+			},
+			expectedEntity: &diode.Device{
+				Name:        stringPtr("router1"),
+				Description: stringPtr("Device specific description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("device-tag1")},
+					{Name: stringPtr("device-tag2")},
+				},
+			},
 			expectError: false,
 		},
 		{
-			name:   "empty values map",
-			values: map[ObjectIDIndex]*ObjectIDValue{},
-			mappingEntry: &mappingEntry{
+			name: "mapping with both global and entity-specific defaults",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.1.5.0": {
+					OID:    "1.3.6.1.2.1.1.5.0",
+					Index:  "0",
+					Parent: "1.3.6.1.2.1.1.5",
+					Value:  "router1",
+					Type:   mapping.OctetString,
+				},
+			},
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.1",
 				Entity: "device",
 				Field:  "_id",
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.1.5",
+						Entity: "device",
+						Field:  "name",
+					},
+				},
 			},
-			expectedEntity: &diode.Device{},
-			expectError:    false,
+			defaults: &config.Defaults{
+				Description: "Global description",
+				Tags:        []string{"global-tag1", "global-tag2"},
+				Device: config.EntityDefaults{
+					Description: "Device specific description",
+					Tags:        []string{"device-tag1", "device-tag2"},
+				},
+			},
+			expectedEntity: &diode.Device{
+				Name:        stringPtr("router1"),
+				Description: stringPtr("Device specific description"),
+				Tags: []*diode.Tag{
+					{Name: stringPtr("device-tag1")},
+					{Name: stringPtr("device-tag2")},
+					{Name: stringPtr("global-tag1")},
+					{Name: stringPtr("global-tag2")},
+				},
+			},
+			expectError: false,
 		},
 		{
-			name: "invalid OID format",
-			values: map[ObjectIDIndex]*ObjectIDValue{
+			name: "mapping with invalid platform OID",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{
+				"1.3.6.1.2.1.1.5.0": {
+					OID:    "1.3.6.1.2.1.1.5.0",
+					Index:  "0",
+					Parent: "1.3.6.1.2.1.1.5",
+					Value:  "router1",
+					Type:   mapping.OctetString,
+				},
 				"1.3.6.1.2.1.1.2.0": {
 					OID:    "1.3.6.1.2.1.1.2.0",
 					Index:  "0",
 					Parent: "1.3.6.1.2.1.1.2",
-					Value:  "invalid.oid",
-					Type:   ObjectIdentifier,
+					Value:  "invalid",
+					Type:   mapping.ObjectIdentifier,
 				},
 			},
-			mappingEntry: &mappingEntry{
+			defaults: nil,
+			mappingEntry: &mapping.Entry{
 				OID:    "1.3.6.1.2.1.1",
 				Entity: "device",
 				Field:  "_id",
-				MappingEntries: []mappingEntry{
+				MappingEntries: []mapping.Entry{
+					{
+						OID:    "1.3.6.1.2.1.1.5",
+						Entity: "device",
+						Field:  "name",
+					},
 					{
 						OID:    "1.3.6.1.2.1.1.2",
 						Entity: "device",
@@ -439,24 +840,27 @@ func TestDeviceMapper_Map(t *testing.T) {
 				},
 			},
 			expectedEntity: &diode.Device{
-				DeviceType: &diode.DeviceType{
-					Manufacturer: &diode.Manufacturer{
-						Name: diode.String("unknown"),
-					},
-					Model: diode.String("unknown"),
-				},
-				Platform: &diode.Platform{
-					Manufacturer: &diode.Manufacturer{
-						Name: diode.String("unknown"),
-					},
-				},
+				Name: stringPtr("router1"),
 			},
 			expectError: false,
 		},
+		{
+			name:   "empty values map",
+			values: map[mapping.ObjectIDIndex]*mapping.ObjectIDValue{},
+			mappingEntry: &mapping.Entry{
+				OID:    "1.3.6.1.2.1.1",
+				Entity: "device",
+				Field:  "_id",
+			},
+			expectedEntity: &diode.Device{},
+			expectError:    false,
+		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
+			registry := mapping.NewEntityRegistry(logger)
+			registry.SetDefaults(tt.defaults)
 			entity := mapper.Map(tt.values, tt.mappingEntry, registry, logger)
 
 			if tt.expectError {
@@ -468,9 +872,20 @@ func TestDeviceMapper_Map(t *testing.T) {
 			device, ok := entity.(*diode.Device)
 			assert.True(t, ok)
 			assert.Equal(t, tt.expectedEntity.Name, device.Name)
-			if tt.expectedEntity.Platform != nil && device.Platform != nil {
+			if tt.expectedEntity.DeviceType != nil {
+				assert.Equal(t, tt.expectedEntity.DeviceType.Manufacturer.Name, device.DeviceType.Manufacturer.Name)
+				assert.Equal(t, tt.expectedEntity.DeviceType.Model, device.DeviceType.Model)
+			}
+			if tt.expectedEntity.Platform != nil {
 				assert.Equal(t, tt.expectedEntity.Platform.Manufacturer.Name, device.Platform.Manufacturer.Name)
 			}
+			assert.Equal(t, tt.expectedEntity.Description, device.Description)
+			if tt.expectedEntity.Tags != nil {
+				assert.Equal(t, len(tt.expectedEntity.Tags), len(device.Tags))
+				for i, tag := range tt.expectedEntity.Tags {
+					assert.Equal(t, tag.Name, device.Tags[i].Name)
+				}
+			}
 		})
 	}
 }
diff --git a/snmp-discovery/mapping/mapping.go b/snmp-discovery/mapping/mapping.go
index 8f2cdff2..66176414 100644
--- a/snmp-discovery/mapping/mapping.go
+++ b/snmp-discovery/mapping/mapping.go
@@ -51,6 +51,7 @@ const (
 type EntityRegistry struct {
 	entities map[EntityType]map[ObjectIDIndex]diode.Entity
 	logger   *slog.Logger
+	defaults *config.Defaults
 }
 
 // NewEntityRegistry creates a new EntityRegistry
@@ -61,6 +62,16 @@ func NewEntityRegistry(logger *slog.Logger) *EntityRegistry {
 	}
 }
 
+// SetDefaults sets the defaults for the registry
+func (r *EntityRegistry) SetDefaults(defaults *config.Defaults) {
+	r.defaults = defaults
+}
+
+// GetDefaults returns the defaults for the registry
+func (r *EntityRegistry) GetDefaults() *config.Defaults {
+	return r.defaults
+}
+
 // GetOrCreateEntity returns an entity from the EntityRegistry or creates a new one if it doesn't exist
 func (r *EntityRegistry) GetOrCreateEntity(entityType EntityType, index ObjectIDIndex) diode.Entity {
 	r.logger.Debug("Getting entity", "entityType", entityType, "index", index, "from", r.entities)
@@ -98,22 +109,24 @@ type EntityType string
 
 // ObjectIDMapper is a struct that maps ObjectIDs to entities
 type ObjectIDMapper struct {
-	mapping  map[string]*mappingEntry
+	mapping  map[string]*Entry
 	logger   *slog.Logger
 	registry *EntityRegistry
 }
 
-type mappingEntry struct {
+// Entry is a struct that contains a mapping entry
+type Entry struct {
 	OID            string
 	Entity         string
 	Field          string
-	MappingEntries []mappingEntry
+	MappingEntries []Entry
 	Mapper         orbToEntityMapper
 	IdentifierSize int
 	Relationship   config.Relationship
 }
 
-func (m *mappingEntry) MapToEntity(pdus map[ObjectIDIndex]*ObjectIDValue, entityRegistry *EntityRegistry, logger *slog.Logger) []diode.Entity {
+// MapToEntity maps a value to an entity
+func (m *Entry) MapToEntity(pdus map[ObjectIDIndex]*ObjectIDValue, entityRegistry *EntityRegistry, logger *slog.Logger) []diode.Entity {
 	logger.Debug("Mapping value to entity", "value", pdus)
 	if m.Mapper == nil {
 		logger.Warn("No mapper found for entity. Ignoring.", "entity", m.Entity)
@@ -129,7 +142,7 @@ func (m *mappingEntry) MapToEntity(pdus map[ObjectIDIndex]*ObjectIDValue, entity
 }
 
 // NewObjectIDMapper creates a new ObjectIDMapper
-func NewObjectIDMapper(mappings []config.MappingEntry, logger *slog.Logger, devices data.DeviceDataRetreiver) *ObjectIDMapper {
+func NewObjectIDMapper(mappings []config.MappingEntry, logger *slog.Logger, devices data.DeviceDataRetreiver, defaults *config.Defaults) *ObjectIDMapper {
 	entityMappers := map[string]orbToEntityMapper{
 		"ipAddress": &IPAddressMapper{},
 		"interface": &InterfaceMapper{},
@@ -137,25 +150,30 @@ func NewObjectIDMapper(mappings []config.MappingEntry, logger *slog.Logger, devi
 			devices: devices,
 		},
 	}
-	mapping := make(map[string]*mappingEntry)
+	mapping := make(map[string]*Entry)
 	for _, m := range mappings {
 		logger.Debug("Adding mapping", "oid", m.OID, "entity", m.Entity, "field", m.Field, "relationship", m.Relationship)
-		mappingEntry := newMappingEntry(m, logger, entityMappers)
-		if mappingEntry == nil {
+		Entry := newMappingEntry(m, logger, entityMappers)
+		if Entry == nil {
 			continue
 		}
-		mapping[m.OID] = mappingEntry
+		mapping[m.OID] = Entry
+	}
+
+	registry := NewEntityRegistry(logger)
+	if defaults != nil {
+		registry.SetDefaults(defaults)
 	}
 
 	return &ObjectIDMapper{
 		mapping:  mapping,
 		logger:   logger,
-		registry: NewEntityRegistry(logger),
+		registry: registry,
 	}
 }
 
 type orbToEntityMapper interface {
-	Map(pdus map[ObjectIDIndex]*ObjectIDValue, mappingEntry *mappingEntry, entityRegistry *EntityRegistry, logger *slog.Logger) diode.Entity
+	Map(pdus map[ObjectIDIndex]*ObjectIDValue, Entry *Entry, entityRegistry *EntityRegistry, logger *slog.Logger) diode.Entity
 }
 
 func getIndex(values map[ObjectIDIndex]*ObjectIDValue) ObjectIDIndex {
@@ -165,13 +183,13 @@ func getIndex(values map[ObjectIDIndex]*ObjectIDValue) ObjectIDIndex {
 	return ""
 }
 
-func newMappingEntry(m config.MappingEntry, logger *slog.Logger, entityMappers map[string]orbToEntityMapper) *mappingEntry {
+func newMappingEntry(m config.MappingEntry, logger *slog.Logger, entityMappers map[string]orbToEntityMapper) *Entry {
 	mapper := entityMappers[m.Entity]
 	if mapper == nil {
 		logger.Warn("No mapper found for entity. Ignoring.", "entity", m.Entity)
 		return nil
 	}
-	return &mappingEntry{
+	return &Entry{
 		OID:            m.OID,
 		Entity:         m.Entity,
 		Field:          m.Field,
@@ -182,11 +200,11 @@ func newMappingEntry(m config.MappingEntry, logger *slog.Logger, entityMappers m
 	}
 }
 
-func newChildMappingEntries(configMappingEntries []config.MappingEntry, logger *slog.Logger) []mappingEntry {
-	childMappingEntries := make([]mappingEntry, 0, len(configMappingEntries))
+func newChildMappingEntries(configMappingEntries []config.MappingEntry, logger *slog.Logger) []Entry {
+	childMappingEntries := make([]Entry, 0, len(configMappingEntries))
 	for _, m := range configMappingEntries {
 		logger.Debug("Adding child mapping entry", "oid", m.OID, "entity", m.Entity, "field", m.Field, "relationship", m.Relationship)
-		child := &mappingEntry{
+		child := &Entry{
 			OID:            m.OID,
 			Entity:         m.Entity,
 			Field:          m.Field,
@@ -236,12 +254,12 @@ func (m *ObjectIDMapper) MapObjectIDsToEntity(objectIDs ObjectIDValueMap) []diod
 	entities := make([]diode.Entity, 0, len(objectIDIndexMap))
 	for index, value := range objectIDIndexMap {
 		m.logger.Debug("Mapping objectIDIndex", "objectIDIndex", index, "values", value.Values)
-		mappingEntry, err := m.getMappingEntry(value.Index)
+		Entry, err := m.getMappingEntry(value.Index)
 		if err != nil {
 			m.logger.Warn("Error finding mapping entry", "error", err, "objectID", value.Index)
 			continue
 		}
-		entities = append(entities, mappingEntry.MapToEntity(value.Values, m.registry, m.logger)...)
+		entities = append(entities, Entry.MapToEntity(value.Values, m.registry, m.logger)...)
 	}
 	return entities
 }
@@ -279,7 +297,7 @@ func newObjectIDValue(objectID string, value Value) (*ObjectIDValue, error) {
 }
 
 // Gets the mapper for the closest parent objectID
-func (m *ObjectIDMapper) getMappingEntry(objectID string) (*mappingEntry, error) {
+func (m *ObjectIDMapper) getMappingEntry(objectID string) (*Entry, error) {
 	for {
 		if value, found := m.mapping[objectID]; found {
 			return value, nil
diff --git a/snmp-discovery/mapping/mapping_test.go b/snmp-discovery/mapping/mapping_test.go
index 4541a06c..16aefee2 100644
--- a/snmp-discovery/mapping/mapping_test.go
+++ b/snmp-discovery/mapping/mapping_test.go
@@ -341,7 +341,12 @@ func TestMapObjectIDsToEntity(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			mapper := mapping.NewObjectIDMapper(tt.mapping, slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false})), &FakeManufacturers{})
+			mapper := mapping.NewObjectIDMapper(
+				tt.mapping,
+				slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false})),
+				&FakeManufacturers{},
+				&config.Defaults{},
+			)
 			entities := mapper.MapObjectIDsToEntity(tt.objectIDs)
 
 			assert.ElementsMatch(t, tt.expected, entities)
@@ -398,7 +403,12 @@ func TestObjectIDs(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			mapper := mapping.NewObjectIDMapper(tt.mapping, slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false})), &FakeManufacturers{})
+			mapper := mapping.NewObjectIDMapper(
+				tt.mapping,
+				slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: false})),
+				&FakeManufacturers{},
+				&config.Defaults{},
+			)
 			objectIDs := mapper.ObjectIDs()
 
 			assert.Equal(t, tt.expectedOIDs, objectIDs)
diff --git a/snmp-discovery/policy/runner.go b/snmp-discovery/policy/runner.go
index 9aa16a87..3b194bb8 100644
--- a/snmp-discovery/policy/runner.go
+++ b/snmp-discovery/policy/runner.go
@@ -85,7 +85,7 @@ func (r *Runner) run() {
 	ctx, cancel := context.WithTimeout(r.ctx, r.timeout)
 	defer cancel()
 
-	mapper := mapping.NewObjectIDMapper(r.scope.Mappings, r.logger, r.manufacturers)
+	mapper := mapping.NewObjectIDMapper(r.scope.Mappings, r.logger, r.manufacturers, &r.config.Defaults)
 	objectIDs := mapper.ObjectIDs()
 	r.logger.Info("Starting SNMP crawl of targets", slog.Any("targetCount", len(r.scope.Targets)), slog.Any("objectCount", len(objectIDs)))
 	entities := make([]diode.Entity, 0)
diff --git a/snmp-discovery/policy/runner_test.go b/snmp-discovery/policy/runner_test.go
index 02052c10..4e2ab563 100644
--- a/snmp-discovery/policy/runner_test.go
+++ b/snmp-discovery/policy/runner_test.go
@@ -122,6 +122,19 @@ func TestRunnerRun(t *testing.T) {
 						Description: "Test",
 						Comments:    "This is a test",
 						Tags:        []string{"test", "snmp"},
+						IPAddress: config.EntityDefaults{
+							Description: "IP Address Default",
+							Comments:    "IP Address Comment",
+							Tags:        []string{"ip", "default"},
+						},
+						Interface: config.EntityDefaults{
+							Description: "Interface Default",
+							Tags:        []string{"interface", "default"},
+						},
+						Device: config.EntityDefaults{
+							Description: "Device Default",
+							Tags:        []string{"device", "default"},
+						},
 					},
 				},
 				Scope: config.Scope{
@@ -188,7 +201,26 @@ func TestRunnerIngestCalledWithCorrectValues(t *testing.T) {
 	ctx := context.Background()
 
 	policyConfig := config.Policy{
-		Config: config.PolicyConfig{},
+		Config: config.PolicyConfig{
+			Defaults: config.Defaults{
+				Description: "Test",
+				Comments:    "This is a test",
+				Tags:        []string{"test", "snmp"},
+				IPAddress: config.EntityDefaults{
+					Description: "IP Address Default",
+					Comments:    "IP Address Comment",
+					Tags:        []string{"ip", "default"},
+				},
+				Interface: config.EntityDefaults{
+					Description: "Interface Default",
+					Tags:        []string{"interface", "default"},
+				},
+				Device: config.EntityDefaults{
+					Description: "Device Default",
+					Tags:        []string{"device", "default"},
+				},
+			},
+		},
 		Scope: config.Scope{
 			Targets: []config.Target{
 				{
@@ -219,7 +251,18 @@ func TestRunnerIngestCalledWithCorrectValues(t *testing.T) {
 	// Use a channel to signal that Ingest was called
 	ingestCalled := make(chan bool, 1)
 
-	expectedEntities := []diode.Entity{&diode.Interface{Name: diode.String("GigabitEthernet1/0/1")}}
+	expectedEntities := []diode.Entity{
+		&diode.Interface{
+			Name:        diode.String("GigabitEthernet1/0/1"),
+			Description: diode.String("Interface Default"),
+			Tags: []*diode.Tag{
+				{Name: diode.String("interface")},
+				{Name: diode.String("default")},
+				{Name: diode.String("test")},
+				{Name: diode.String("snmp")},
+			},
+		},
+	}
 
 	mockClient.On("Ingest", mock.Anything, expectedEntities).Run(func(args mock.Arguments) {
 		ingestCalled <- true
@@ -233,14 +276,14 @@ func TestRunnerIngestCalledWithCorrectValues(t *testing.T) {
 	// Wait for Ingest to be called or timeout
 	select {
 	case <-ingestCalled:
-		// Success
+		// Ingest was called, proceed
 	case <-time.After(10 * time.Second):
 		t.Fatal("Timeout: Ingest was not called")
 	}
 
 	// Stop the process
 	err = runner.Stop()
-	assert.NoError(t, err)
+	assert.NoError(t, err, "Runner.Stop should not return an error")
 }
 
 func TestRunnerWalkError(t *testing.T) {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request enhances the SNMP discovery service by introducing support for global and entity-specific default values and simplifying mapper instantiation. Key changes include adding the EntityDefaults type to configuration, updating mapping and policy runner logic to apply defaults, and updating tests and documentation to reflect these changes.

### Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

<details>
<summary>Show a summary per file</summary>

| File                                             | Description                                                              |
| ------------------------------------------------ | ------------------------------------------------------------------------ |
| snmp-discovery/policy/runner_test.go             | Tests updated to include default configuration values for IPAddress, Interface, and Device. |
| snmp-discovery/policy/runner.go                  | Updated mapper instantiation to pass global defaults.                    |
| snmp-discovery/mapping/mapping_test.go           | Modified tests to pass defaults in ObjectIDMapper creation.              |
| snmp-discovery/mapping/mappers_test.go           | Adjusted test cases to use the updated MappingEntry type and package references. |
| snmp-discovery/mapping/mappers.go                | Updated mapper functions to apply default values and refactored variable naming. |
| snmp-discovery/config/config.go                  | Introduced the EntityDefaults struct and added default fields to Defaults. |
| snmp-discovery/README.md                         | Expanded documentation with a YAML configuration example including defaults. |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**snmp-discovery/mapping/mapping.go:156**
* [nitpick] The variable name 'MappingEntry' is capitalized, which can be confusing as it resembles a type name. Consider renaming it to 'mappingEntry' for clarity and to follow Go naming conventions.
```
MappingEntry := newMappingEntry(m, logger, entityMappers)
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T19:41:25Z
user: login: jajeffries
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T09:38:01Z
user: login: leoparente
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:03:27Z
user: login: leoparente
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:03:42Z
user: login: leoparente
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:57:11Z

Review comments:
diff_hunk: @@ -47,24 +50,62 @@ func (m *IPAddressMapper) Map(values map[ObjectIDIndex]*ObjectIDValue, mappingEn
 			}
 		}
 	}
+
+	// Apply defaults if available
+	if defaults := entityRegistry.GetDefaults(); defaults != nil && fieldFound {
path: snmp-discovery/mapping/mappers.go
user: login: jajeffries
body: This duplicated block will be extracted into a common method in an upcoming PR
created_at: 2025-05-22T09:38:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,3 +20,117 @@ options:
   -a DIODE_APP_NAME_PREFIX, --diode-app-name-prefix DIODE_APP_NAME_PREFIX
                         Diode producer_app_name prefix
 ```
+
+## Configuration
+
+The SNMP discovery service is configured using a YAML file. The configuration file has the following structure:
+
+```yaml
+config:
+  schedule: "*/5 * * * *"  # Optional: Cron expression for scheduling
+  defaults:  # Optional: Default values for entities
+    description: "Global description"  # Optional: Global description for all entities
+    comments: "Global comments"  # Optional: Global comments for all entities
+    tags:  # Optional: Global tags for all entities
+      - "global"
+      - "snmp"
+    ipAddress:  # Optional: Defaults specific to IP addresses
path: snmp-discovery/README.md
user: login: leoparente
body: any reason for that not be snake case as we discussed before on notion?
created_at: 2025-05-22T12:03:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,3 +20,117 @@ options:
   -a DIODE_APP_NAME_PREFIX, --diode-app-name-prefix DIODE_APP_NAME_PREFIX
                         Diode producer_app_name prefix
 ```
+
+## Configuration
+
+The SNMP discovery service is configured using a YAML file. The configuration file has the following structure:
+
+```yaml
+config:
+  schedule: "*/5 * * * *"  # Optional: Cron expression for scheduling
+  defaults:  # Optional: Default values for entities
+    description: "Global description"  # Optional: Global description for all entities
+    comments: "Global comments"  # Optional: Global comments for all entities
+    tags:  # Optional: Global tags for all entities
+      - "global"
+      - "snmp"
+    ipAddress:  # Optional: Defaults specific to IP addresses
+      description: "IP Address description"
+      comments: "IP Address comments"
+      tags:
+        - "ip"
+        - "default"
+    interface:  # Optional: Defaults specific to interfaces
+      description: "Interface description"
+      tags:
+        - "interface"
+        - "default"
+    device:  # Optional: Defaults specific to devices
+      description: "Device description"
+      tags:
+        - "device"
+        - "default"
+scope:
+  targets:  # List of SNMP targets to discover
+    - host: "192.168.1.1"  # Required: Hostname or IP address
+      port: 161  # Optional: SNMP port (default: 161)
+  authentication:  # SNMP authentication settings
+    protocolVersion: "2c"  # Required: SNMP protocol version ("1", "2c", or "3")
path: snmp-discovery/README.md
user: login: leoparente
body: same here
created_at: 2025-05-22T12:03:42Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-21T19:43:03Z
body: Go test coverage
| Status  | Elapsed |                          Package                           | Cover  | Pass | Fail | Skip |
|---------|---------|------------------------------------------------------------|--------|------|------|------|
| 🟢 PASS |  1.01s  | github.com/netboxlabs/orb-discovery/snmp-discovery/config  | 100.0% |  8   |  0   |  0   |
| 🟢 PASS |  1.01s  | github.com/netboxlabs/orb-discovery/snmp-discovery/data    | 82.6%  |  8   |  0   |  0   |
| 🟢 PASS |  1.04s  | github.com/netboxlabs/orb-discovery/snmp-discovery/mapping | 89.0%  |  33  |  0   |  0   |
| 🟢 PASS |  7.04s  | github.com/netboxlabs/orb-discovery/snmp-discovery/policy  | 74.5%  |  14  |  0   |  0   |
| 🟢 PASS |  1.04s  | github.com/netboxlabs/orb-discovery/snmp-discovery/server  | 86.0%  |  21  |  0   |  0   |
| 🟢 PASS |  1.02s  | github.com/netboxlabs/orb-discovery/snmp-discovery/snmp    | 84.7%  |  29  |  0   |  0   |
| 🟢 PASS |  1.01s  | github.com/netboxlabs/orb-discovery/snmp-discovery/version | 100.0% |  1   |  0   |  0   |
Total coverage: 84.4%
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T17:02:35Z
body: :tada: This PR is included in version 1.2.0 :tada:

The release is available on [GitHub release](https://github.com/netboxlabs/orb-discovery/releases/tag/device-discovery/v1.2.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T17:04:43Z
body: :tada: This PR is included in version 1.2.0 :tada:

The release is available on [GitHub release](https://github.com/netboxlabs/orb-discovery/releases/tag/network-discovery/v1.2.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-26T13:59:13Z
body: :tada: This PR is included in version 1.1.0 :tada:

The release is available on [GitHub release](https://github.com/netboxlabs/orb-discovery/releases/tag/worker/v1.1.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-28T20:01:26Z
body: :tada: This PR is included in version 1.0.0 :tada:

The release is available on [GitHub release](https://github.com/netboxlabs/orb-discovery/releases/tag/snmp-discovery/v1.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: NONE