Skip to content

Commit

Permalink
added owned node capability in API (#164)
Browse files Browse the repository at this point in the history
added owned node capability in API
  • Loading branch information
irshadaj authored Oct 25, 2023
1 parent 392c41c commit 5d6a10f
Show file tree
Hide file tree
Showing 5 changed files with 145 additions and 20 deletions.
99 changes: 95 additions & 4 deletions cmd/api/src/api/agi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ func TestResources_UpdateAssetGroupSelectors_PayloadError(t *testing.T) {
require.Contains(t, response.Body.String(), api.ErrorResponsePayloadUnmarshalError)
}

func TestResources_UpdateAssetGroupSelectors_Success(t *testing.T) {
func TestResources_UpdateAssetGroupSelectors_SuccessT0(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

Expand Down Expand Up @@ -489,9 +489,9 @@ func TestResources_UpdateAssetGroupSelectors_Success(t *testing.T) {
req = mux.SetURLVars(req, map[string]string{api.URIPathVariableAssetGroupID: "1"})

assetGroup := model.AssetGroup{
Name: "test group",
Tag: "test tag",
SystemGroup: false,
Name: model.TierZeroAssetGroupName,
Tag: model.TierZeroAssetGroupTag,
SystemGroup: true,
}

expectedResult := map[string]model.AssetGroupSelectors{
Expand All @@ -516,6 +516,8 @@ func TestResources_UpdateAssetGroupSelectors_Success(t *testing.T) {
mockDB.EXPECT().UpdateAssetGroupSelectors(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedResult, nil)

mockTasker := datapipeMocks.NewMockTasker(mockCtrl)
// MockTasker should receive a call to RequestAnalysis() since this is a Tier Zero Asset group.
// Analysis must be run upon updating a T0 AG
mockTasker.EXPECT().RequestAnalysis()

handlers := v2.Resources{
Expand Down Expand Up @@ -543,3 +545,92 @@ func TestResources_UpdateAssetGroupSelectors_Success(t *testing.T) {
require.Equal(t, expectedResult["added_selectors"][0].Name, data["added_selectors"][0].Name)
require.Equal(t, expectedResult["removed_selectors"][0].Name, data["removed_selectors"][0].Name)
}

func TestResources_UpdateAssetGroupSelectors_SuccessOwned(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

payload := []model.AssetGroupSelectorSpec{
{
SelectorName: "test",
EntityObjectID: "1",
Action: model.SelectorSpecActionAdd,
},
{
SelectorName: "test2",
EntityObjectID: "2",
Action: model.SelectorSpecActionRemove,
},
}

req, err := http.NewRequest("POST", "/api/v2/asset-groups/1/selectors", must.MarshalJSONReader(payload))
require.Nil(t, err)

req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String())

bheCtx := ctx.Context{
RequestID: "requestID",
AuthCtx: auth.Context{
Owner: model.User{},
Session: model.UserSession{},
},
}
req = req.WithContext(context.WithValue(context.Background(), ctx.ValueKey, bheCtx.WithRequestID("requestID")))
req = mux.SetURLVars(req, map[string]string{api.URIPathVariableAssetGroupID: "1"})

assetGroup := model.AssetGroup{
Name: model.OwnedAssetGroupName,
Tag: model.OwnedAssetGroupTag,
SystemGroup: true,
}

expectedResult := map[string]model.AssetGroupSelectors{
"added_selectors": {
model.AssetGroupSelector{
AssetGroupID: assetGroup.ID,
Name: payload[0].SelectorName,
Selector: payload[0].EntityObjectID,
},
},
"removed_selectors": {
model.AssetGroupSelector{
AssetGroupID: assetGroup.ID,
Name: payload[1].SelectorName,
Selector: payload[1].EntityObjectID,
},
},
}

mockDB := dbMocks.NewMockDatabase(mockCtrl)
mockDB.EXPECT().GetAssetGroup(gomock.Any()).Return(assetGroup, nil)
mockDB.EXPECT().UpdateAssetGroupSelectors(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedResult, nil)

mockTasker := datapipeMocks.NewMockTasker(mockCtrl)
// NOTE MockTasker should NOT receive a call to RequestAnalysis() since this is not a Tier Zero Asset group.
// Analysis should not be re-run when a non T0 AG is updated

handlers := v2.Resources{
DB: mockDB,
TaskNotifier: mockTasker,
}

response := httptest.NewRecorder()
handler := http.HandlerFunc(handlers.UpdateAssetGroupSelectors)

handler.ServeHTTP(response, req)
require.Equal(t, http.StatusOK, response.Code)

resp := api.ResponseWrapper{}
err = json.Unmarshal(response.Body.Bytes(), &resp)
require.Nil(t, err)

dataJSON, err := json.Marshal(resp.Data)
require.Nil(t, err)

data := make(map[string][]model.AssetGroupSelector, 0)
err = json.Unmarshal(dataJSON, &data)
require.Nil(t, err)

require.Equal(t, expectedResult["added_selectors"][0].Name, data["added_selectors"][0].Name)
require.Equal(t, expectedResult["removed_selectors"][0].Name, data["removed_selectors"][0].Name)
}
6 changes: 4 additions & 2 deletions cmd/api/src/api/v2/agi.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,10 @@ func (s Resources) UpdateAssetGroupSelectors(response http.ResponseWriter, reque
if result, err := s.DB.UpdateAssetGroupSelectors(*ctx.FromRequest(request), assetGroup, selectorSpecs, false); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
// When asset group selectors are modified we must trigger analysis
s.TaskNotifier.RequestAnalysis()
// When T0 asset group selectors are modified we must trigger analysis
if assetGroup.Tag == model.TierZeroAssetGroupTag {
s.TaskNotifier.RequestAnalysis()
}

api.WriteBasicResponse(request.Context(), result, http.StatusOK, response)
}
Expand Down
37 changes: 23 additions & 14 deletions cmd/api/src/database/migration/agi.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,27 @@
// Copyright 2023 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 migration

import (
"regexp"

"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/src/model"
"gorm.io/gorm"
"github.com/specterops/bloodhound/log"
)

const (
tierZeroAssetGroupName = "Admin Tier Zero"
tierZeroAssetGroupTag = "admin_tier_0"
)

var (
Expand Down Expand Up @@ -55,13 +50,13 @@ func (s *Migrator) updateAssetGroups() error {
return result.Error
}

// Create the tier zero asset group if it doesn't already exist
if _, hasTierZero := systemAssetGroups.FindByName(tierZeroAssetGroupName); !hasTierZero {
// Create asset groups if they don't already exist
if _, hasTierZero := systemAssetGroups.FindByName(model.TierZeroAssetGroupName); !hasTierZero {
log.Infof("Missing the default Admin Tier Zero asset group. Creating it now.")

newTierZeroAG := model.AssetGroup{
Name: tierZeroAssetGroupName,
Tag: tierZeroAssetGroupTag,
Name: model.TierZeroAssetGroupName,
Tag: model.TierZeroAssetGroupTag,
SystemGroup: true,
}

Expand All @@ -70,6 +65,20 @@ func (s *Migrator) updateAssetGroups() error {
}
}

if _, hasOwned := systemAssetGroups.FindByName(model.OwnedAssetGroupTag); !hasOwned {
log.Infof("Missing the default Owned asset group. Creating it now.")

ownedAG := model.AssetGroup{
Name: model.OwnedAssetGroupName,
Tag: model.OwnedAssetGroupTag,
SystemGroup: true,
}

if result := tx.Create(&ownedAG); result.Error != nil {
return result.Error
}
}

// Load the AG selectors to migrate the selectors away from cypher
for _, assetGroup := range systemAssetGroups {
var selectors model.AssetGroupSelectors
Expand Down
19 changes: 19 additions & 0 deletions cmd/api/src/database/migration/migrations/v5.1.1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Copyright 2023 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

INSERT INTO asset_groups (name, tag, system_group)
SELECT 'Owned', 'owned', true
WHERE NOT EXISTS (SELECT 1 FROM asset_groups WHERE tag='owned')
4 changes: 4 additions & 0 deletions cmd/api/src/model/agi.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,10 @@ type AssetGroupSelectorSpec struct {
const (
SelectorSpecActionAdd = "add"
SelectorSpecActionRemove = "remove"
TierZeroAssetGroupName = "Admin Tier Zero"
TierZeroAssetGroupTag = "admin_tier_0"
OwnedAssetGroupName = "Owned"
OwnedAssetGroupTag = "owned"
)

func (s AssetGroupSelectorSpec) Validate() error {
Expand Down

0 comments on commit 5d6a10f

Please sign in to comment.