Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: filter node property at ingest and for entity panel display #810

Merged
merged 1 commit into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions cmd/api/src/daemons/datapipe/ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (

const (
IngestCountThreshold = 500
ReconcileProperty = "reconcile"
)

func ReadFileForIngest(batch graph.Batch, reader io.ReadSeeker, adcsEnabled bool) error {
Expand Down Expand Up @@ -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(),
Expand Down
48 changes: 48 additions & 0 deletions cmd/api/src/daemons/datapipe/ingest_test.go
Original file line number Diff line number Diff line change
@@ -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()])
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(<ObjectInfoFields fields={properties} />);

expect(screen.getByText('Object ID')).toBeInTheDocument();

exclusionList.forEach((property) => {
expect(screen.queryByText(property)).not.toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -32,6 +32,7 @@ const exclusionList = [
'displayname',
'service_principal_id',
'highvalue',
'reconcile',
];

const filterNegatedFields = (fields: EntityField[]): EntityField[] =>
Expand Down
Loading