Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
695ab80
update func name to align with others
LawsonWillard Dec 3, 2025
ed6bead
just generate
LawsonWillard Dec 3, 2025
e260134
Merge branch 'refs/heads/main' into BED-6791
LawsonWillard Dec 3, 2025
ff48cdc
wip
LawsonWillard Dec 3, 2025
b090df9
Merge branch 'main' into BED-6791
LawsonWillard Dec 3, 2025
5bfbfbd
refactor node and edge functions and models to use graph in name to c…
LawsonWillard Dec 3, 2025
b9f301c
add ToSqlString for sort direction function
LawsonWillard Dec 3, 2025
72de6cd
add GetSchemaNodeKinds function
LawsonWillard Dec 4, 2025
ecb4fc0
refactor SchemaNodeKind integration tests and add tests for GetSchema…
LawsonWillard Dec 4, 2025
ec18319
remove space
LawsonWillard Dec 4, 2025
6dc3669
Merge branch 'main' into BED-6791
LawsonWillard Dec 8, 2025
948c2cb
Merge branch 'main' into BED-6791
LawsonWillard Dec 9, 2025
6b6f387
update GetGraphSchemaNodeKinds and tests to use new filtering/sorting
LawsonWillard Dec 9, 2025
62ac318
update count function return value
LawsonWillard Dec 9, 2025
bc42d4e
update test
LawsonWillard Dec 9, 2025
375300b
update test name
LawsonWillard Dec 9, 2025
7ee0014
update comparison helper func
LawsonWillard Dec 9, 2025
b0d01a4
wip
LawsonWillard Dec 9, 2025
2b78991
Merge branch 'main' into BED-6792
LawsonWillard Dec 9, 2025
489b3ac
add GetGraphSchemaEdgeKinds and tests/mocks
LawsonWillard Dec 9, 2025
4ffc208
Merge branch 'main' into BED-6792
LawsonWillard Dec 10, 2025
8a8012e
rename function test
LawsonWillard Dec 10, 2025
842f92e
update doc string
LawsonWillard Dec 10, 2025
92ef455
refactor tests and add ability to update extension id in UpdateProper…
LawsonWillard Dec 10, 2025
8f2fdaf
Add GetGraphSchemaProperties and corresponding tests
LawsonWillard Dec 10, 2025
0a0c345
add new func to interface and generate mocks
LawsonWillard Dec 10, 2025
7a95d7c
refactor by removing unnecesary structs in node kind integration tests
LawsonWillard Dec 10, 2025
d60e005
Merge branch 'main' into BED-6784
LawsonWillard Dec 10, 2025
b5a843c
add function that was missed in merge
LawsonWillard Dec 10, 2025
0a01124
refactor edgeKind integration tests to not use unnecesary structs
LawsonWillard Dec 10, 2025
66bc5eb
rabbit suggestions
LawsonWillard Dec 11, 2025
28b7a60
Merge branch 'main' into BED-6784
LawsonWillard Dec 11, 2025
af11640
WIP Extensions Endpoint
LawsonWillard Dec 11, 2025
b159bb5
Merge branch 'main' into BED-6723
LawsonWillard Dec 11, 2025
c2a9ada
Merge branch 'refs/heads/BED-6784' into BED-6723
LawsonWillard Dec 11, 2025
3117abc
wip
LawsonWillard Dec 11, 2025
2e166ed
refactor diffing function for better clarity
LawsonWillard Dec 12, 2025
6b9e6d6
refactor UpsertGraphSchemaExtension to reduce cyclomatic complexity
LawsonWillard Dec 12, 2025
f34f965
minor refactor to api resouce and api entry point func
LawsonWillard Dec 12, 2025
c68dcca
save generated test file
LawsonWillard Dec 12, 2025
eb043cb
Merge branch 'main' into BED-6723
LawsonWillard Dec 12, 2025
9c3634c
ensure separation of service and API layer for opengraph
LawsonWillard Dec 15, 2025
3167433
wip
LawsonWillard Dec 15, 2025
76f9f81
Merge branch 'main' into BED-6723
LawsonWillard Dec 15, 2025
23926ff
Merge branch 'refs/heads/main' into BED-6723
LawsonWillard Dec 17, 2025
36b55d4
wip
LawsonWillard Dec 18, 2025
de9be78
Merge branch 'main' into BED-6723
LawsonWillard Dec 18, 2025
0a37479
fix test
LawsonWillard Dec 18, 2025
cf27f95
Merge branch 'refs/heads/main' into BED-6723
LawsonWillard Dec 24, 2025
6c5af88
add feature flag
LawsonWillard Dec 24, 2025
583849f
add feature flag
LawsonWillard Dec 24, 2025
1d4427f
update mocks
LawsonWillard Dec 24, 2025
7a5ec49
create service layer mock
LawsonWillard Dec 24, 2025
666b785
update api layer
LawsonWillard Dec 24, 2025
afa0fcd
update model
LawsonWillard Dec 24, 2025
8fe8e2b
refactor open graph service layer and tests
LawsonWillard Dec 24, 2025
8dc9362
update based on static analysis
LawsonWillard Dec 24, 2025
07cf978
Merge branch 'main' into BED-6723
LawsonWillard Jan 7, 2026
dec52fc
push upsert logic down, tests are broken :/
LawsonWillard Jan 9, 2026
a1d26bd
Merge branch 'main' into BED-6723
LawsonWillard Jan 9, 2026
88eafcb
Merge branch 'main' into BED-6723
LawsonWillard Jan 14, 2026
fa6c3c2
Merge branch 'refs/heads/main' into BED-6723
LawsonWillard Jan 15, 2026
0419db4
add v8.6.0 migration and feature flag
LawsonWillard Jan 15, 2026
f575e01
migrate upsert logic to db layer and add initial tests
LawsonWillard Jan 15, 2026
1e76403
add feature flag
LawsonWillard Jan 15, 2026
3feca08
update entrypoint
LawsonWillard Jan 15, 2026
e58f808
update service layer logic, tests and mocks
LawsonWillard Jan 15, 2026
5f17a95
update opengraph models
LawsonWillard Jan 15, 2026
22f36b0
update api layer
LawsonWillard Jan 15, 2026
09f0d9d
update v8.5.0
LawsonWillard Jan 15, 2026
a79bcf3
update upsert due to linting issues
LawsonWillard Jan 15, 2026
25e73af
wip: add open api docs
LawsonWillard Jan 15, 2026
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
4 changes: 3 additions & 1 deletion cmd/api/src/api/registration/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func RegisterFossRoutes(
authenticator api.Authenticator,
authorizer auth.Authorizer,
ingestSchema upload.IngestSchema,
openGraphSchemaService v2.OpenGraphSchemaService,
dogtagsService dogtags.Service,
) {
router.With(func() mux.MiddlewareFunc {
Expand All @@ -82,6 +83,7 @@ func RegisterFossRoutes(
routerInst.PathPrefix("/ui", static.AssetHandler),
)

var resources = v2.NewResources(rdms, graphDB, cfg, apiCache, graphQuery, collectorManifests, authorizer, authenticator, ingestSchema, dogtagsService)
var resources = v2.NewResources(rdms, graphDB, cfg, apiCache, graphQuery, collectorManifests, authorizer,
authenticator, ingestSchema, openGraphSchemaService, dogtagsService)
NewV2API(resources, routerInst)
}
3 changes: 3 additions & 0 deletions cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,5 +367,8 @@ func NewV2API(resources v2.Resources, routerInst *router.Router) {
routerInst.POST("/api/v2/custom-nodes", resources.CreateCustomNodeKind).RequireAuth(),
routerInst.PUT(fmt.Sprintf("/api/v2/custom-nodes/{%s}", v2.CustomNodeKindParameter), resources.UpdateCustomNodeKind).RequireAuth(),
routerInst.DELETE(fmt.Sprintf("/api/v2/custom-nodes/{%s}", v2.CustomNodeKindParameter), resources.DeleteCustomNodeKind).RequireAuth(),

// Open Graph Schema Ingest
routerInst.PUT("/api/v2/extensions", resources.OpenGraphSchemaIngest).RequireAuth(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is configuration related, I think it might be a good idea to require a permission related to
"write" for this endpoint

Copy link
Contributor Author

@LawsonWillard LawsonWillard Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure which permission set would be best, this will be admin only, do you think its worth creating a new set of admin permissions for graph schema management?

)
}
73 changes: 73 additions & 0 deletions cmd/api/src/api/v2/mocks/graphschemaextensions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions cmd/api/src/api/v2/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ type Resources struct {
IngestSchema upload.IngestSchema
FileService fs.Service
DogTags dogtags.Service
openGraphSchemaService OpenGraphSchemaService
}

func NewResources(
Expand All @@ -129,6 +130,7 @@ func NewResources(
authorizer auth.Authorizer,
authenticator api.Authenticator,
ingestSchema upload.IngestSchema,
openGraphSchemaService OpenGraphSchemaService,
dogtagsService dogtags.Service,
) Resources {
return Resources{
Expand All @@ -144,6 +146,7 @@ func NewResources(
Authenticator: authenticator,
IngestSchema: ingestSchema,
FileService: &fs.Client{},
openGraphSchemaService: openGraphSchemaService,
DogTags: dogtagsService,
}
}
117 changes: 117 additions & 0 deletions cmd/api/src/api/v2/opengraphschema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2025 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 v2

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/specterops/bloodhound/cmd/api/src/api"
"github.com/specterops/bloodhound/cmd/api/src/auth"
ctx2 "github.com/specterops/bloodhound/cmd/api/src/ctx"
"github.com/specterops/bloodhound/cmd/api/src/model"
"github.com/specterops/bloodhound/cmd/api/src/model/appcfg"
"github.com/specterops/bloodhound/cmd/api/src/model/ingest"
bhUtils "github.com/specterops/bloodhound/cmd/api/src/utils"
"github.com/specterops/bloodhound/packages/go/headers"
"github.com/specterops/bloodhound/packages/go/mediatypes"
)

//go:generate go run go.uber.org/mock/mockgen -copyright_file ../../../../../LICENSE.header -destination=./mocks/graphschemaextensions.go -package=mocks . OpenGraphSchemaService

type OpenGraphSchemaService interface {
UpsertOpenGraphExtension(ctx context.Context, graphSchema model.GraphSchema) (bool, error)
}

func (s Resources) OpenGraphSchemaIngest(response http.ResponseWriter, request *http.Request) {
var (
ctx = request.Context()
err error
flag appcfg.FeatureFlag

updated bool

extractExtensionData func(file io.Reader) (model.GraphSchema, error)
graphSchemaPayload model.GraphSchema
)

// TODO: what to return if feature flag is not enabled
if flag, err = s.DB.GetFlagByKey(ctx, appcfg.FeatureOpenGraphExtensionManagement); err != nil {
api.HandleDatabaseError(request, response, err)
} else if !flag.Enabled {
response.WriteHeader(http.StatusNotFound)
} else if user, isUser := auth.GetUserFromAuthCtx(ctx2.FromRequest(request).AuthCtx); !isUser {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "No associated "+
"user found", request), response)
} else if !user.Roles.Has(model.Role{Name: auth.RoleAdministrator}) {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, "user does not "+
"have sufficient permissions to create or update an extension", request), response)
} else if request.Body == nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "open graph "+
"extension payload cannot be empty", request), response)
} else {
request.Body = http.MaxBytesReader(response, request.Body, api.DefaultAPIPayloadReadLimitBytes)
defer request.Body.Close()
switch {
case bhUtils.HeaderMatches(request.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()):
extractExtensionData = extractExtensionDataFromJSON
case bhUtils.HeaderMatches(request.Header, headers.ContentType.String(), ingest.AllowedZipFileUploadTypes...):
fallthrough
// extractExtensionData = extractExtensionDataFromZipFile - will be needed for a future
default:
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusUnsupportedMediaType,
fmt.Sprintf("%s; Content type must be application/json",
fmt.Errorf("invalid content-type: %s", request.Header[headers.ContentType.String()])), request), response)
return
}

if graphSchemaPayload, err = extractExtensionData(request.Body); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response)
return
}

if updated, err = s.openGraphSchemaService.UpsertOpenGraphExtension(ctx, graphSchemaPayload); err != nil {
switch {
// TODO: more error types (ex: validation)
default:
api.WriteErrorResponse(ctx, api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf("unable to update graph schema: %v", err), request), response)
return
}
} else if updated {
response.WriteHeader(http.StatusOK)
} else {
response.WriteHeader(http.StatusCreated)
}
}
}

// extractExtensionDataFromJSON - extracts a model.GraphSchema from the incoming payload. Will return an error if there
// are any extra fields or if the decoder fails to decode the payload.
func extractExtensionDataFromJSON(payload io.Reader) (model.GraphSchema, error) {
var (
err error
decoder = json.NewDecoder(payload)
graphSchema model.GraphSchema
)
decoder.DisallowUnknownFields()
if err = decoder.Decode(&graphSchema); err != nil {
return graphSchema, err
}
return graphSchema, nil
}
3 changes: 1 addition & 2 deletions cmd/api/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,7 @@ type Database interface {
// Environment Targeted Access Control
EnvironmentTargetedAccessControlData

// OpenGraph Schema
OpenGraphSchema
GetGraphSchemaEdgeKindsWithSchemaName(ctx context.Context, edgeKindFilters model.Filters, sort model.Sort, skip, limit int) (model.GraphSchemaEdgeKindsWithNamedSchema, int, error)
}

type BloodhoundDB struct {
Expand Down
Loading