From ba22f444c1ed0a97275aae48f1b56d7ddfced4d9 Mon Sep 17 00:00:00 2001 From: Ulises Rangel Date: Tue, 10 Sep 2024 09:53:20 -0500 Subject: [PATCH] fix: filter node property at ingest and for entity panel display (#810) --- cmd/api/src/daemons/datapipe/ingest.go | 33 +++++++------ cmd/api/src/daemons/datapipe/ingest_test.go | 48 +++++++++++++++++++ .../src/views/Explore/fragments.test.tsx | 21 +++++++- .../src/views/Explore/fragments.tsx | 3 +- 4 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 cmd/api/src/daemons/datapipe/ingest_test.go diff --git a/cmd/api/src/daemons/datapipe/ingest.go b/cmd/api/src/daemons/datapipe/ingest.go index bd786a6560..2823fdbc56 100644 --- a/cmd/api/src/daemons/datapipe/ingest.go +++ b/cmd/api/src/daemons/datapipe/ingest.go @@ -35,6 +35,7 @@ import ( const ( IngestCountThreshold = 500 + ReconcileProperty = "reconcile" ) func ReadFileForIngest(batch graph.Batch, reader io.ReadSeeker, adcsEnabled bool) error { @@ -134,40 +135,44 @@ func IngestWrapper(batch graph.Batch, reader io.ReadSeeker, meta ingest.Metadata return nil } -func IngestNode(batch graph.Batch, nowUTC time.Time, identityKind graph.Kind, nextNode ein.IngestibleNode) error { - //Ensure object id is upper case - nextNode.ObjectID = strings.ToUpper(nextNode.ObjectID) - - nextNode.PropertyMap[common.LastSeen.String()] = nowUTC - nextNode.PropertyMap[common.ObjectID.String()] = nextNode.ObjectID +func NormalizeEinNodeProperties(properties map[string]any, objectID string, nowUTC time.Time) map[string]any { + delete(properties, ReconcileProperty) + properties[common.LastSeen.String()] = nowUTC + properties[common.ObjectID.String()] = strings.ToUpper(objectID) - //Ensure that name, operatingsystem, and distinguishedname properties are upper case - if rawName, hasName := nextNode.PropertyMap[common.Name.String()]; hasName && rawName != nil { + // Ensure that name, operatingsystem, and distinguishedname properties are upper case + if rawName, hasName := properties[common.Name.String()]; hasName && rawName != nil { if name, typeMatches := rawName.(string); typeMatches { - nextNode.PropertyMap[common.Name.String()] = strings.ToUpper(name) + properties[common.Name.String()] = strings.ToUpper(name) } else { log.Errorf("Bad type found for node name property during ingest. Expected string, got %T", rawName) } } - if rawOS, hasOS := nextNode.PropertyMap[common.OperatingSystem.String()]; hasOS && rawOS != nil { + if rawOS, hasOS := properties[common.OperatingSystem.String()]; hasOS && rawOS != nil { if os, typeMatches := rawOS.(string); typeMatches { - nextNode.PropertyMap[common.OperatingSystem.String()] = strings.ToUpper(os) + properties[common.OperatingSystem.String()] = strings.ToUpper(os) } else { log.Errorf("Bad type found for node operating system property during ingest. Expected string, got %T", rawOS) } } - if rawDN, hasDN := nextNode.PropertyMap[ad.DistinguishedName.String()]; hasDN && rawDN != nil { + if rawDN, hasDN := properties[ad.DistinguishedName.String()]; hasDN && rawDN != nil { if dn, typeMatches := rawDN.(string); typeMatches { - nextNode.PropertyMap[ad.DistinguishedName.String()] = strings.ToUpper(dn) + properties[ad.DistinguishedName.String()] = strings.ToUpper(dn) } else { log.Errorf("Bad type found for node distinguished name property during ingest. Expected string, got %T", rawDN) } } + return properties +} + +func IngestNode(batch graph.Batch, nowUTC time.Time, identityKind graph.Kind, nextNode ein.IngestibleNode) error { + normalizedProperties := NormalizeEinNodeProperties(nextNode.PropertyMap, nextNode.ObjectID, nowUTC) + return batch.UpdateNodeBy(graph.NodeUpdate{ - Node: graph.PrepareNode(graph.AsProperties(nextNode.PropertyMap), nextNode.Label), + Node: graph.PrepareNode(graph.AsProperties(normalizedProperties), nextNode.Label), IdentityKind: identityKind, IdentityProperties: []string{ common.ObjectID.String(), diff --git a/cmd/api/src/daemons/datapipe/ingest_test.go b/cmd/api/src/daemons/datapipe/ingest_test.go new file mode 100644 index 0000000000..23da4a7a3c --- /dev/null +++ b/cmd/api/src/daemons/datapipe/ingest_test.go @@ -0,0 +1,48 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package datapipe_test + +import ( + "testing" + "time" + + "github.com/specterops/bloodhound/graphschema/ad" + "github.com/specterops/bloodhound/graphschema/common" + "github.com/specterops/bloodhound/src/daemons/datapipe" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeEinNodeProperties(t *testing.T) { + var ( + nowUTC = time.Now().UTC() + objectID = "objectid" + properties = map[string]any{ + datapipe.ReconcileProperty: false, + common.Name.String(): "name", + common.OperatingSystem.String(): "temple", + ad.DistinguishedName.String(): "distinguished-name", + } + normalizedProperties = datapipe.NormalizeEinNodeProperties(properties, objectID, nowUTC) + ) + + assert.Nil(t, normalizedProperties[datapipe.ReconcileProperty]) + assert.NotNil(t, normalizedProperties[common.LastSeen.String()]) + assert.Equal(t, "OBJECTID", normalizedProperties[common.ObjectID.String()]) + assert.Equal(t, "NAME", normalizedProperties[common.Name.String()]) + assert.Equal(t, "DISTINGUISHED-NAME", normalizedProperties[ad.DistinguishedName.String()]) + assert.Equal(t, "TEMPLE", normalizedProperties[common.OperatingSystem.String()]) +} diff --git a/packages/javascript/bh-shared-ui/src/views/Explore/fragments.test.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.test.tsx index e4d347596c..e78b68458e 100644 --- a/packages/javascript/bh-shared-ui/src/views/Explore/fragments.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.test.tsx @@ -14,8 +14,9 @@ // // SPDX-License-Identifier: Apache-2.0 -import { Field } from './fragments'; import { render, screen } from '../../test-utils'; +import { EntityField } from '../../utils'; +import { exclusionList, Field, ObjectInfoFields } from './fragments'; describe('Field', () => { it('should render a Field when the provided value is false', () => { @@ -44,3 +45,21 @@ describe('Field', () => { expect(container.innerHTML).toBe(''); }); }); + +describe('ObjectInfoFields', () => { + it('should filter properties that we do not want to display in the entity panel', () => { + const properties: EntityField[] = exclusionList.map((key) => { + return { label: key, value: 'test', keyprop: key }; + }); + const validProperty: EntityField = { label: 'Object ID', value: 'OBJECT_ID' }; + properties.push(validProperty); + + render(); + + expect(screen.getByText('Object ID')).toBeInTheDocument(); + + exclusionList.forEach((property) => { + expect(screen.queryByText(property)).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx index d901a21645..15709a2077 100644 --- a/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Explore/fragments.tsx @@ -19,7 +19,7 @@ import useCollapsibleSectionStyles from './InfoStyles/CollapsibleSection'; import React, { PropsWithChildren } from 'react'; import { EntityField, format } from '../../utils'; -const exclusionList = [ +export const exclusionList = [ 'gid', 'admin_rights_count', 'admin_rights_risk_percent', @@ -32,6 +32,7 @@ const exclusionList = [ 'displayname', 'service_principal_id', 'highvalue', + 'reconcile', ]; const filterNegatedFields = (fields: EntityField[]): EntityField[] =>