From 4e7a23d5d5a8aca401caff391708b02290dd13c8 Mon Sep 17 00:00:00 2001 From: caffix Date: Sat, 9 Nov 2024 20:27:21 +0000 Subject: [PATCH] added entity and edge tags --- assetdb_test.go | 30 ++++ repository/entity_test.go | 73 ++------- repository/models.go | 65 ++++++-- repository/models_test.go | 10 -- repository/repository.go | 10 +- repository/tag.go | 323 ++++++++++++++++++++++++++++++++++++++ repository/tag_test.go | 162 +++++++++++++++++++ types/types.go | 6 +- 8 files changed, 599 insertions(+), 80 deletions(-) create mode 100644 repository/tag.go create mode 100644 repository/tag_test.go diff --git a/assetdb_test.go b/assetdb_test.go index f32e304..67377af 100644 --- a/assetdb_test.go +++ b/assetdb_test.go @@ -468,3 +468,33 @@ func (m *mockAssetDB) OutgoingEdges(asset *types.Entity, since time.Time, labels args := m.Called(asset, since, labels) return args.Get(0).([]*types.Edge), args.Error(1) } + +func (m *mockAssetDB) CreateEntityTag(entity *types.Entity, property oam.Property) (*types.EntityTag, error) { + args := m.Called(entity, property) + return args.Get(0).(*types.EntityTag), args.Error(1) +} + +func (m *mockAssetDB) GetEntityTags(entity *types.Entity, since time.Time, names ...string) ([]*types.EntityTag, error) { + args := m.Called(entity, since, names) + return args.Get(0).([]*types.EntityTag), args.Error(1) +} + +func (m *mockAssetDB) DeleteEntityTag(id string) error { + args := m.Called(id) + return args.Error(0) +} + +func (m *mockAssetDB) CreateEdgeTag(edge *types.Edge, property oam.Property) (*types.EdgeTag, error) { + args := m.Called(edge, property) + return args.Get(0).(*types.EdgeTag), args.Error(1) +} + +func (m *mockAssetDB) GetEdgeTags(edge *types.Edge, since time.Time, names ...string) ([]*types.EdgeTag, error) { + args := m.Called(edge, since, names) + return args.Get(0).([]*types.EdgeTag), args.Error(1) +} + +func (m *mockAssetDB) DeleteEdgeTag(id string) error { + args := m.Called(id) + return args.Error(0) +} diff --git a/repository/entity_test.go b/repository/entity_test.go index ca7f4e0..f3383e1 100644 --- a/repository/entity_test.go +++ b/repository/entity_test.go @@ -175,7 +175,7 @@ func TestMain(m *testing.M) { func TestLastSeenUpdates(t *testing.T) { ip, _ := netip.ParseAddr("45.73.25.1") - asset := network.IPAddress{Address: ip, Type: "IPv4"} + asset := &network.IPAddress{Address: ip, Type: "IPv4"} a1, err := store.CreateEntity(asset) assert.NoError(t, err) @@ -240,22 +240,12 @@ func TestRepository(t *testing.T) { for _, tc := range testCases { t.Run(tc.description, func(t *testing.T) { sourceEntity, err := store.CreateEntity(tc.sourceAsset) - if err != nil { - t.Fatalf("failed to create entity: %s", err) - } - - if sourceEntity == nil { - t.Fatalf("failed to create entity: entity is nil") - } + assert.NoError(t, err) + assert.NotEqual(t, sourceEntity, nil) foundAsset, err := store.FindEntityById(sourceEntity.ID, start) - if err != nil { - t.Fatalf("failed to find entity by id: %s", err) - } - - if foundAsset == nil { - t.Fatalf("failed to find entity by id: found entity is nil") - } + assert.NoError(t, err) + assert.NotEqual(t, foundAsset, nil) if foundAsset.ID != sourceEntity.ID { t.Fatalf("failed to find entity by id: expected entity id %s, got %s", sourceEntity.ID, foundAsset.ID) @@ -266,13 +256,8 @@ func TestRepository(t *testing.T) { } foundAssetByContent, err := store.FindEntityByContent(sourceEntity.Asset, start) - if err != nil { - t.Fatalf("failed to find entity by content: %s", err) - } - - if foundAssetByContent == nil { - t.Fatalf("failed to find entity by content: found entity is nil") - } + assert.NoError(t, err) + assert.NotEqual(t, foundAssetByContent, nil) if foundAssetByContent[0].ID != sourceEntity.ID { t.Fatalf("failed to find entity by content: expected entity id %s, got %s", sourceEntity.ID, foundAssetByContent[0].ID) @@ -314,13 +299,8 @@ func TestRepository(t *testing.T) { } destinationEntity, err := store.CreateEntity(tc.destinationAsset) - if err != nil { - t.Fatalf("failed to create destination entity: %s", err) - } - - if destinationEntity == nil { - t.Fatalf("failed to create destination entity: destination entity is nil") - } + assert.NoError(t, err) + assert.NotEqual(t, destinationEntity, nil) edge := &types.Edge{ Relation: tc.relation, @@ -329,22 +309,12 @@ func TestRepository(t *testing.T) { } e, err := store.Link(edge) - if err != nil { - t.Fatalf("failed to link entities: %s", err) - } - - if e == nil { - t.Fatalf("failed to link entities: edge is nil") - } + assert.NoError(t, err) + assert.NotEqual(t, e, nil) incoming, err := store.IncomingEdges(destinationEntity, start, tc.relation.Label()) - if err != nil { - t.Fatalf("failed to query incoming edges: %s", err) - } - - if incoming == nil { - t.Fatalf("failed to query incoming edges: incoming edge is nil %s", err) - } + assert.NoError(t, err) + assert.NotEqual(t, incoming, nil) if incoming[0].Relation.Label() != tc.relation.Label() { t.Fatalf("failed to query incoming edges: expected relation %s, got %s", tc.relation, incoming[0].Relation.Label()) @@ -359,13 +329,8 @@ func TestRepository(t *testing.T) { } outgoing, err := store.OutgoingEdges(sourceEntity, start, tc.relation.Label()) - if err != nil { - t.Fatalf("failed to query outgoing edges: %s", err) - } - - if outgoing == nil { - t.Fatalf("failed to query outgoing edges: outgoing edge is nil") - } + assert.NoError(t, err) + assert.NotEqual(t, outgoing, nil) if outgoing[0].Relation.Label() != tc.relation.Label() { t.Fatalf("failed to query outgoing edges: expected edge %s, got %s", tc.relation, outgoing[0].Relation.Label()) @@ -380,14 +345,10 @@ func TestRepository(t *testing.T) { } err = store.DeleteEdge(e.ID) - if err != nil { - t.Fatalf("failed to delete edges: %s", err) - } + assert.NoError(t, err) err = store.DeleteEntity(destinationEntity.ID) - if err != nil { - t.Fatalf("failed to delete asset: %s", err) - } + assert.NoError(t, err) if _, err = store.FindEntityById(destinationEntity.ID, start); err == nil { t.Fatal("failed to delete entity: the entity was not removed from the database") diff --git a/repository/models.go b/repository/models.go index c11f3ac..02f974b 100644 --- a/repository/models.go +++ b/repository/models.go @@ -14,10 +14,10 @@ import ( "github.com/owasp-amass/open-asset-model/contact" "github.com/owasp-amass/open-asset-model/domain" oamfile "github.com/owasp-amass/open-asset-model/file" - "github.com/owasp-amass/open-asset-model/fingerprint" "github.com/owasp-amass/open-asset-model/network" "github.com/owasp-amass/open-asset-model/org" "github.com/owasp-amass/open-asset-model/people" + "github.com/owasp-amass/open-asset-model/property" oamreg "github.com/owasp-amass/open-asset-model/registration" "github.com/owasp-amass/open-asset-model/relation" "github.com/owasp-amass/open-asset-model/service" @@ -34,11 +34,20 @@ type Entity struct { Content datatypes.JSON } +// EntityTag represents additional metadata added to an entity in the asset database. +type EntityTag struct { + ID uint64 `gorm:"primaryKey;column:tag_id"` + CreatedAt time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();column:created_at"` + LastSeen time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();column:last_seen"` + Type string `gorm:"column:ttype"` + Content datatypes.JSON +} + // Edge represents a relationship between two entities stored in the database. type Edge struct { ID uint64 `gorm:"primaryKey;column:edge_id"` - CreatedAt time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();"` - LastSeen time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();"` + CreatedAt time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();column:created_at"` + LastSeen time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();column:last_seen"` Type string `gorm:"column:etype"` Content datatypes.JSON FromEntityID uint64 `gorm:"column:from_entity_id"` @@ -47,6 +56,15 @@ type Edge struct { ToEntity Entity } +// EdgeTag represents additional metadata added to an edge in the asset database. +type EdgeTag struct { + ID uint64 `gorm:"primaryKey;column:tag_id"` + CreatedAt time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();column:created_at"` + LastSeen time.Time `gorm:"type:datetime;default:CURRENT_TIMESTAMP();column:last_seen"` + Type string `gorm:"column:ttype"` + Content datatypes.JSON +} + // Parse parses the content of the entity into the corresponding Open Asset Model (OAM) asset type. // It returns the parsed asset and an error, if any. func (e *Entity) Parse() (oam.Asset, error) { @@ -89,11 +107,6 @@ func (e *Entity) Parse() (oam.Asset, error) { err = json.Unmarshal(e.Content, &dr) asset = &dr - case string(oam.Fingerprint): - var fingerprint fingerprint.Fingerprint - - err = json.Unmarshal(e.Content, &fingerprint) - asset = &fingerprint case string(oam.Organization): var organization org.Organization @@ -175,8 +188,6 @@ func (e *Entity) JSONQuery() (*datatypes.JSONQueryExpression, error) { return jsonQuery.Equals(v.Handle, "handle"), nil case *oamreg.DomainRecord: return jsonQuery.Equals(v.Domain, "domain"), nil - case *fingerprint.Fingerprint: - return jsonQuery.Equals(v.Value, "value"), nil case *org.Organization: return jsonQuery.Equals(v.Name, "name"), nil case *people.Person: @@ -240,3 +251,37 @@ func (e *Edge) Parse() (oam.Relation, error) { return rel, err } + +// Parse parses the content of the entity tag into the corresponding Open Asset Model (OAM) property type. +// It returns the parsed property and an error, if any. +func (e *EntityTag) Parse() (oam.Property, error) { + return parseProperty(e.Type, e.Content) +} + +// Parse parses the content of the edge tag into the corresponding Open Asset Model (OAM) property type. +// It returns the parsed property and an error, if any. +func (e *EdgeTag) Parse() (oam.Property, error) { + return parseProperty(e.Type, e.Content) +} + +func parseProperty(ptype string, content datatypes.JSON) (oam.Property, error) { + var err error + var prop oam.Property + + switch ptype { + case string(oam.SimpleProperty): + var sp property.SimpleProperty + + err = json.Unmarshal(content, &sp) + prop = &sp + case string(oam.VulnProperty): + var vp property.VulnProperty + + err = json.Unmarshal(content, &vp) + prop = &vp + default: + return nil, fmt.Errorf("unknown property type: %s", ptype) + } + + return prop, err +} diff --git a/repository/models_test.go b/repository/models_test.go index a989fbf..97359cd 100644 --- a/repository/models_test.go +++ b/repository/models_test.go @@ -14,7 +14,6 @@ import ( oamcert "github.com/owasp-amass/open-asset-model/certificate" "github.com/owasp-amass/open-asset-model/contact" "github.com/owasp-amass/open-asset-model/domain" - "github.com/owasp-amass/open-asset-model/fingerprint" "github.com/owasp-amass/open-asset-model/network" "github.com/owasp-amass/open-asset-model/org" "github.com/owasp-amass/open-asset-model/people" @@ -87,10 +86,6 @@ func TestModels(t *testing.T) { description: "parse location", asset: &contact.Location{Address: "1600 Pennsylvania Ave NW, Washington, DC 20500"}, }, - { - description: "parse fingerprint", - asset: &fingerprint.Fingerprint{Value: "a1:2b:3c:4d:5e:6f:7g:8h:9i:0j:1k:2l:3m:4n:5o:6p"}, - }, { description: "parse organization", asset: &org.Organization{Name: "Example, Inc."}, @@ -183,11 +178,6 @@ func TestModels(t *testing.T) { asset: &contact.Location{Address: "1600 Pennsylvania Ave NW, Washington, DC 20500"}, expectedQuery: datatypes.JSONQuery("content").Equals("1600 Pennsylvania Ave NW, Washington, DC 20500", "address"), }, - { - description: "json query for fingerprint", - asset: &fingerprint.Fingerprint{Value: "a1:2b:3c:4d:5e:6f:7g:8h:9i:0j:1k:2l:3m:4n:5o:6p"}, - expectedQuery: datatypes.JSONQuery("content").Equals("a1:2b:3c:4d:5e:6f:7g:8h:9i:0j:1k:2l:3m:4n:5o:6p", "value"), - }, { description: "json query for url", asset: &url.URL{Raw: "https://www.example.com"}, diff --git a/repository/repository.go b/repository/repository.go index 32ac9cb..357b3f9 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -24,7 +24,13 @@ type Repository interface { FindEntitiesByType(atype oam.AssetType, since time.Time) ([]*types.Entity, error) FindEntitiesByScope(constraints []oam.Asset, since time.Time) ([]*types.Entity, error) Link(edge *types.Edge) (*types.Edge, error) - IncomingEdges(asset *types.Entity, since time.Time, labels ...string) ([]*types.Edge, error) - OutgoingEdges(asset *types.Entity, since time.Time, labels ...string) ([]*types.Edge, error) + IncomingEdges(entity *types.Entity, since time.Time, labels ...string) ([]*types.Edge, error) + OutgoingEdges(entity *types.Entity, since time.Time, labels ...string) ([]*types.Edge, error) + CreateEntityTag(entity *types.Entity, property oam.Property) (*types.EntityTag, error) + GetEntityTags(entity *types.Entity, since time.Time, names ...string) ([]*types.EntityTag, error) + DeleteEntityTag(id string) error + CreateEdgeTag(edge *types.Edge, property oam.Property) (*types.EdgeTag, error) + GetEdgeTags(edge *types.Edge, since time.Time, names ...string) ([]*types.EdgeTag, error) + DeleteEdgeTag(id string) error Close() error } diff --git a/repository/tag.go b/repository/tag.go new file mode 100644 index 0000000..0dd5f71 --- /dev/null +++ b/repository/tag.go @@ -0,0 +1,323 @@ +// Copyright © by Jeff Foley 2017-2024. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. +// SPDX-License-Identifier: Apache-2.0 + +package repository + +import ( + "strconv" + "time" + + "github.com/owasp-amass/asset-db/types" + oam "github.com/owasp-amass/open-asset-model" + "gorm.io/gorm" +) + +func (sql *sqlRepository) CreateEntityTag(entity *types.Entity, prop oam.Property) (*types.EntityTag, error) { + jsonContent, err := prop.JSON() + if err != nil { + return nil, err + } + + tag := EntityTag{ + Type: string(prop.PropertyType()), + Content: jsonContent, + } + + // ensure that duplicate entity tags are not entered into the database + if tags, err := sql.GetEntityTags(entity, time.Time{}, prop.Name()); err == nil && len(tags) > 0 { + for _, t := range tags { + if prop.PropertyType() == t.Property.PropertyType() && prop.Value() == t.Property.Value() { + if id, err := strconv.ParseUint(t.ID, 10, 64); err == nil { + tag.ID = id + tag.CreatedAt = t.CreatedAt + + if sql.UpdateEntityTagLastSeen(t.ID) == nil { + if f, err := sql.FindEntityTagById(t.ID, time.Time{}); err == nil && f != nil { + tag.LastSeen = f.LastSeen + break + } + } + } + } + } + } + + result := sql.db.Save(&tag) + if result.Error != nil { + return nil, result.Error + } + + return &types.EntityTag{ + ID: strconv.FormatUint(tag.ID, 10), + CreatedAt: tag.CreatedAt, + LastSeen: tag.LastSeen, + Property: prop, + }, nil +} + +// UpdateEntityTagLastSeen performs an update on the entity tag. +func (sql *sqlRepository) UpdateEntityTagLastSeen(id string) error { + result := sql.db.Exec("UPDATE entity_tags SET last_seen = current_timestamp WHERE tag_id = ?", id) + if result.Error != nil { + return result.Error + } + return nil +} + +// FindEntityTagById finds an entity tag in the database by its ID and last seen after the since parameter. +// It takes a string representing the entity tag ID and retrieves the corresponding tag from the database. +// If since.IsZero(), the parameter will be ignored. +// Returns the discovered tag as a types.EntityTag or an error if the asset is not found. +func (sql *sqlRepository) FindEntityTagById(id string, since time.Time) (*types.EntityTag, error) { + tagId, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return &types.EntityTag{}, err + } + + var result *gorm.DB + tag := EntityTag{ID: tagId} + if since.IsZero() { + result = sql.db.First(&tag) + } else { + result = sql.db.Where("last_seen > ?", since).First(&tag) + } + if result.Error != nil { + return &types.EntityTag{}, result.Error + } + + data, err := tag.Parse() + if err != nil { + return &types.EntityTag{}, err + } + + return &types.EntityTag{ + ID: strconv.FormatUint(tag.ID, 10), + CreatedAt: tag.CreatedAt, + LastSeen: tag.LastSeen, + Property: data, + }, nil +} + +// GetEntityTags finds all tag for the entity with the specified names and last seen after the since parameter. +// If since.IsZero(), the parameter will be ignored. +// If no names are specified, all tags for the specified entity are returned. +func (sql *sqlRepository) GetEntityTags(entity *types.Entity, since time.Time, names ...string) ([]*types.EntityTag, error) { + entityId, err := strconv.ParseInt(entity.ID, 10, 64) + if err != nil { + return nil, err + } + + var tags []EntityTag + var result *gorm.DB + if since.IsZero() { + result = sql.db.Where("entity_id = ?", entityId).Find(&tags) + } else { + result = sql.db.Where("entity_id = ? AND last_seen > ?", entityId, since).Find(&tags) + } + if err := result.Error; err != nil { + return nil, err + } + + var results []*types.EntityTag + for _, tag := range tags { + t := &tag + + if prop, err := t.Parse(); err == nil { + found := true + + if len(names) > 0 { + found = false + n := prop.Name() + + for _, name := range names { + if name == n { + found = true + break + } + } + } + + if found { + results = append(results, &types.EntityTag{ + ID: strconv.Itoa(int(t.ID)), + CreatedAt: t.CreatedAt, + LastSeen: t.LastSeen, + Property: prop, + }) + } + } + } + + return results, nil +} + +// DeleteEntityTag removes an entity tag in the database by its ID. +// It takes a string representing the entity tag ID and removes the corresponding tag from the database. +// Returns an error if the tag is not found. +func (sql *sqlRepository) DeleteEntityTag(id string) error { + tagId, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return err + } + + tag := EntityTag{ID: tagId} + result := sql.db.Delete(&tag) + if result.Error != nil { + return result.Error + } + return nil +} + +func (sql *sqlRepository) CreateEdgeTag(edge *types.Edge, prop oam.Property) (*types.EdgeTag, error) { + jsonContent, err := prop.JSON() + if err != nil { + return nil, err + } + + tag := EdgeTag{ + Type: string(prop.PropertyType()), + Content: jsonContent, + } + + // ensure that duplicate edge tags are not entered into the database + if tags, err := sql.GetEdgeTags(edge, time.Time{}, prop.Name()); err == nil && len(tags) > 0 { + for _, t := range tags { + if prop.PropertyType() == t.Property.PropertyType() && prop.Value() == t.Property.Value() { + if id, err := strconv.ParseUint(t.ID, 10, 64); err == nil { + tag.ID = id + tag.CreatedAt = t.CreatedAt + + if sql.UpdateEdgeTagLastSeen(t.ID) == nil { + if f, err := sql.FindEdgeTagById(t.ID, time.Time{}); err == nil && f != nil { + tag.LastSeen = f.LastSeen + break + } + } + } + } + } + } + + result := sql.db.Save(&tag) + if result.Error != nil { + return nil, result.Error + } + + return &types.EdgeTag{ + ID: strconv.FormatUint(tag.ID, 10), + CreatedAt: tag.CreatedAt, + LastSeen: tag.LastSeen, + Property: prop, + }, nil +} + +// UpdateEdgeTagLastSeen performs an update on the edge tag. +func (sql *sqlRepository) UpdateEdgeTagLastSeen(id string) error { + result := sql.db.Exec("UPDATE edge_tags SET last_seen = current_timestamp WHERE tag_id = ?", id) + if result.Error != nil { + return result.Error + } + return nil +} + +// FindEdgeTagById finds an edge tag in the database by its ID and last seen after the since parameter. +// It takes a string representing the edge tag ID and retrieves the corresponding tag from the database. +// If since.IsZero(), the parameter will be ignored. +// Returns the discovered tag as a types.EdgeTag or an error if the asset is not found. +func (sql *sqlRepository) FindEdgeTagById(id string, since time.Time) (*types.EdgeTag, error) { + tagId, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return &types.EdgeTag{}, err + } + + var result *gorm.DB + tag := EntityTag{ID: tagId} + if since.IsZero() { + result = sql.db.First(&tag) + } else { + result = sql.db.Where("last_seen > ?", since).First(&tag) + } + if result.Error != nil { + return &types.EdgeTag{}, result.Error + } + + data, err := tag.Parse() + if err != nil { + return &types.EdgeTag{}, err + } + + return &types.EdgeTag{ + ID: strconv.FormatUint(tag.ID, 10), + CreatedAt: tag.CreatedAt, + LastSeen: tag.LastSeen, + Property: data, + }, nil +} + +func (sql *sqlRepository) GetEdgeTags(edge *types.Edge, since time.Time, names ...string) ([]*types.EdgeTag, error) { + edgeId, err := strconv.ParseInt(edge.ID, 10, 64) + if err != nil { + return nil, err + } + + var tags []EdgeTag + var result *gorm.DB + if since.IsZero() { + result = sql.db.Where("edge_id = ?", edgeId).Find(&tags) + } else { + result = sql.db.Where("edge_id = ? AND last_seen > ?", edgeId, since).Find(&tags) + } + if err := result.Error; err != nil { + return nil, err + } + + var results []*types.EdgeTag + for _, tag := range tags { + t := &tag + + if prop, err := t.Parse(); err == nil { + found := true + + if len(names) > 0 { + found = false + n := prop.Name() + + for _, name := range names { + if name == n { + found = true + break + } + } + } + + if found { + results = append(results, &types.EdgeTag{ + ID: strconv.Itoa(int(t.ID)), + CreatedAt: t.CreatedAt, + LastSeen: t.LastSeen, + Property: prop, + }) + } + } + } + + return results, nil +} + +// DeleteEdgeTag removes an edge tag in the database by its ID. +// It takes a string representing the edge tag ID and removes the corresponding tag from the database. +// Returns an error if the tag is not found. +func (sql *sqlRepository) DeleteEdgeTag(id string) error { + tagId, err := strconv.ParseUint(id, 10, 64) + if err != nil { + return err + } + + tag := EdgeTag{ID: tagId} + result := sql.db.Delete(&tag) + if result.Error != nil { + return result.Error + } + return nil +} diff --git a/repository/tag_test.go b/repository/tag_test.go new file mode 100644 index 0000000..8ffcf7e --- /dev/null +++ b/repository/tag_test.go @@ -0,0 +1,162 @@ +// Copyright © by Jeff Foley 2017-2024. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. +// SPDX-License-Identifier: Apache-2.0 + +package repository + +import ( + "testing" + "time" + + "github.com/owasp-amass/asset-db/types" + oam "github.com/owasp-amass/open-asset-model" + "github.com/owasp-amass/open-asset-model/domain" + "github.com/owasp-amass/open-asset-model/property" + "github.com/owasp-amass/open-asset-model/relation" + "github.com/stretchr/testify/assert" +) + +func TestEntityTag(t *testing.T) { + entity, err := store.CreateEntity(&domain.FQDN{Name: "utica.edu"}) + assert.NoError(t, err) + + now := time.Now().UTC() + prop := &property.SimpleProperty{ + PropertyName: "test", + PropertyValue: "foo", + } + + ct, err := store.CreateEntityTag(entity, prop) + assert.NoError(t, err) + assert.Equal(t, ct.Property.Name(), prop.PropertyName) + assert.Equal(t, ct.Property.Value(), prop.PropertyValue) + assert.Equal(t, oam.SimpleProperty, ct.Property.PropertyType()) + if now.After(ct.CreatedAt.UTC()) { + t.Errorf("tag.CreatedAt: %s, expected to be after: %s", ct.CreatedAt.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano)) + } + if now.After(ct.LastSeen.UTC()) { + t.Errorf("tag.LastSeen: %s, expected to be after: %s", ct.LastSeen.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano)) + } + + tag, err := store.FindEntityTagById(ct.ID, now) + assert.NoError(t, err) + assert.Equal(t, ct.CreatedAt, tag.CreatedAt) + assert.Equal(t, ct.LastSeen, tag.LastSeen) + assert.Equal(t, ct.Property.Name(), tag.Property.Name()) + assert.Equal(t, ct.Property.Value(), tag.Property.Value()) + + ct2, err := store.CreateEntityTag(entity, prop) + assert.NoError(t, err) + if ct2.LastSeen.UnixNano() <= ct.LastSeen.UnixNano() { + t.Errorf("ct2.LastSeen: %s, ct.LastSeen: %s", ct2.LastSeen.Format(time.RFC3339Nano), ct.LastSeen.Format(time.RFC3339Nano)) + } + + prop.PropertyValue = "bar" + ct3, err := store.CreateEntityTag(entity, prop) + assert.NoError(t, err) + assert.Equal(t, ct3.Property.Value(), prop.PropertyValue) + if ct3.CreatedAt.UnixNano() <= ct2.CreatedAt.UnixNano() { + t.Errorf("ct3.CreatedAt: %s, ct2.CreatedAt: %s", ct3.CreatedAt.Format(time.RFC3339Nano), ct2.CreatedAt.Format(time.RFC3339Nano)) + } + if ct3.LastSeen.UnixNano() <= ct2.LastSeen.UnixNano() { + t.Errorf("ct3.LastSeen: %s, ct2.LastSeen: %s", ct3.LastSeen.Format(time.RFC3339Nano), ct2.LastSeen.Format(time.RFC3339Nano)) + } + + tags, err := store.GetEntityTags(entity, now, "test") + assert.NoError(t, err) + + var found bool + for _, etag := range tags { + if etag.Property.Value() == prop.PropertyValue { + found = true + break + } + } + assert.NotEqual(t, found, true) + + err = store.DeleteEntityTag(ct3.ID) + assert.NoError(t, err) + + tag, err = store.FindEntityTagById(ct3.ID, time.Time{}) + assert.Error(t, err) + assert.Equal(t, tag, nil) +} + +func TestCreateEdgeTag(t *testing.T) { + e1, err := store.CreateEntity(&domain.FQDN{Name: "owasp.org"}) + assert.NoError(t, err) + + e2, err := store.CreateEntity(&domain.FQDN{Name: "www.owasp.org"}) + assert.NoError(t, err) + + edge, err := store.Link(&types.Edge{ + Relation: &relation.BasicDNSRelation{ + Name: "owasp.org", + Header: relation.RRHeader{RRType: 5}, + }, + FromEntity: e1, + ToEntity: e2, + }) + assert.NoError(t, err) + + now := time.Now().UTC() + prop := &property.SimpleProperty{ + PropertyName: "test", + PropertyValue: "foo", + } + + ct, err := store.CreateEdgeTag(edge, prop) + assert.NoError(t, err) + assert.Equal(t, ct.Property.Name(), prop.PropertyName) + assert.Equal(t, ct.Property.Value(), prop.PropertyValue) + assert.Equal(t, oam.SimpleProperty, ct.Property.PropertyType()) + if now.After(ct.CreatedAt.UTC()) { + t.Errorf("tag.CreatedAt: %s, expected to be after: %s", ct.CreatedAt.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano)) + } + if now.After(ct.LastSeen.UTC()) { + t.Errorf("tag.LastSeen: %s, expected to be after: %s", ct.LastSeen.Format(time.RFC3339Nano), now.Format(time.RFC3339Nano)) + } + + tag, err := store.FindEdgeTagById(ct.ID, now) + assert.NoError(t, err) + assert.Equal(t, ct.CreatedAt, tag.CreatedAt) + assert.Equal(t, ct.LastSeen, tag.LastSeen) + assert.Equal(t, ct.Property.Name(), tag.Property.Name()) + assert.Equal(t, ct.Property.Value(), tag.Property.Value()) + + ct2, err := store.CreateEdgeTag(edge, prop) + assert.NoError(t, err) + if ct2.LastSeen.UnixNano() <= ct.LastSeen.UnixNano() { + t.Errorf("ct2.LastSeen: %s, ct.LastSeen: %s", ct2.LastSeen.Format(time.RFC3339Nano), ct.LastSeen.Format(time.RFC3339Nano)) + } + + prop.PropertyValue = "bar" + ct3, err := store.CreateEdgeTag(edge, prop) + assert.NoError(t, err) + assert.Equal(t, ct3.Property.Value(), prop.PropertyValue) + if ct3.CreatedAt.UnixNano() <= ct2.CreatedAt.UnixNano() { + t.Errorf("ct3.CreatedAt: %s, ct2.CreatedAt: %s", ct3.CreatedAt.Format(time.RFC3339Nano), ct2.CreatedAt.Format(time.RFC3339Nano)) + } + if ct3.LastSeen.UnixNano() <= ct2.LastSeen.UnixNano() { + t.Errorf("ct3.LastSeen: %s, ct2.LastSeen: %s", ct3.LastSeen.Format(time.RFC3339Nano), ct2.LastSeen.Format(time.RFC3339Nano)) + } + + tags, err := store.GetEdgeTags(edge, now, "test") + assert.NoError(t, err) + + var found bool + for _, etag := range tags { + if etag.Property.Value() == prop.PropertyValue { + found = true + break + } + } + assert.NotEqual(t, found, true) + + err = store.DeleteEdgeTag(ct3.ID) + assert.NoError(t, err) + + tag, err = store.FindEdgeTagById(ct3.ID, time.Time{}) + assert.Error(t, err) + assert.Equal(t, tag, nil) +} diff --git a/types/types.go b/types/types.go index 1c4c666..c97d313 100644 --- a/types/types.go +++ b/types/types.go @@ -18,11 +18,12 @@ type Entity struct { Asset oam.Asset } -// EntityTag represents addition metadata added to an entity in the asset database. +// EntityTag represents additional metadata added to an entity in the asset database. type EntityTag struct { ID string CreatedAt time.Time LastSeen time.Time + Property oam.Property } // Edge represents a relationship between two entities in the asset database. @@ -35,9 +36,10 @@ type Edge struct { ToEntity *Entity } -// EdgeTag represents addition metadata added to an entity in the asset database. +// EdgeTag represents additional metadata added to an edge in the asset database. type EdgeTag struct { ID string CreatedAt time.Time LastSeen time.Time + Property oam.Property }