Skip to content

Commit

Permalink
Merge branch 'main' of github.com:SpecterOps/BloodHound into BED-5133
Browse files Browse the repository at this point in the history
  • Loading branch information
benwaples committed Jan 3, 2025
2 parents bb3d08e + 2d7bf4b commit 59a0c4b
Show file tree
Hide file tree
Showing 131 changed files with 2,590 additions and 1,267 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/build-ui.yml
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

name: Build UI

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]

jobs:
build-ui:
runs-on: ubuntu-latest

steps:
- name: Checkout source code for this repository
uses: actions/checkout@v3

- name: Install Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install Yarn
run: |
npm install --global yarn
- name: Install Deps
run: |
cd cmd/ui && yarn
- name: Run Build
run: |
cd cmd/ui && yarn build
32 changes: 9 additions & 23 deletions .golangci.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
{
"linters": {
"disable": [
"errcheck"
],
"disable": [],
"enable": [
"gosimple",
"stylecheck"
Expand All @@ -14,17 +12,19 @@
"exclude-rules": [
{
"path": ".go",
"text": "((neo4j(.+)(NewDriver|Result))|Id|database.Database|(.+)Deprecated) is deprecated"
"text": "((neo4j(.+)(NewDriver|Result))|Id|database.Database|(.+)Deprecated|batch.CreateRelationshipByIDs|jwt.StandardClaims) is deprecated"
},
{
"path": "hyperloglog_bench_test.go",
"text": "SA6002:"
},
{
"path": "cache_test\\.go",
"text": "SA1026:",
"severity": "warning"
"text": "SA1026:"
},
{
"path": "foldr_test\\.go",
"text": "SA4000:",
"severity": "warning"
"text": "SA4000:"
},
{
"path": "dawgs/util/size/(.+)",
Expand All @@ -45,21 +45,7 @@
"default-severity": "error",
"rules": [
{
"linters": ["stylecheck", "gosimple", "unused", "errcheck", "forcetypeassert"],
"severity": "warning"
},
{
"text": "SA1019:",
"severity": "warning"
},
{
"path": "hyperloglog_bench_test\\.go",
"text": "SA6002:",
"severity": "warning"
},
{
"path": "expected_ingest.go",
"text": "ST1022:",
"linters": ["errcheck"],
"severity": "warning"
}
]
Expand Down
Binary file not shown.
Binary file not shown.
4 changes: 2 additions & 2 deletions cmd/api/src/api/agi.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,11 @@ func (s AssetGroupMembers) Filter(filterMap model.QueryParameterFilterMap) (Asse
result := s
for column, filters := range filterMap {
if validPredicates, err := s.GetValidFilterPredicatesAsStrings(column); err != nil {
return AssetGroupMembers{}, fmt.Errorf("%s: %s", model.ErrorResponseDetailsColumnNotFilterable, column)
return AssetGroupMembers{}, fmt.Errorf("%s: %s", model.ErrResponseDetailsColumnNotFilterable, column)
} else {
for _, filter := range filters {
if !slices.Contains(validPredicates, string(filter.Operator)) {
return AssetGroupMembers{}, fmt.Errorf("%s: %s, %s", model.ErrorResponseDetailsFilterPredicateNotSupported, column, string(filter.Operator))
return AssetGroupMembers{}, fmt.Errorf("%s: %s, %s", model.ErrResponseDetailsFilterPredicateNotSupported, column, string(filter.Operator))
} else if conditional, err := s.BuildFilteringConditional(column, filter.Operator, filter.Value); err != nil {
return AssetGroupMembers{}, err
} else {
Expand Down
4 changes: 2 additions & 2 deletions cmd/api/src/api/agi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func TestAssetGroupMembers_Filter_Equals(t *testing.T) {
},
})
require.NotNil(t, err)
require.Contains(t, err.Error(), model.ErrorResponseDetailsColumnNotFilterable)
require.Contains(t, err.Error(), model.ErrResponseDetailsColumnNotFilterable)

_, err = input.Filter(model.QueryParameterFilterMap{
"object_id": model.QueryParameterFilters{
Expand All @@ -144,7 +144,7 @@ func TestAssetGroupMembers_Filter_Equals(t *testing.T) {
},
})
require.NotNil(t, err)
require.Contains(t, err.Error(), model.ErrorResponseDetailsFilterPredicateNotSupported)
require.Contains(t, err.Error(), model.ErrResponseDetailsFilterPredicateNotSupported)

// filter on object_id
output, err := input.Filter(model.QueryParameterFilterMap{
Expand Down
20 changes: 10 additions & 10 deletions cmd/api/src/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -34,7 +35,6 @@ import (
"github.com/gofrs/uuid"
"github.com/golang-jwt/jwt/v4"
"github.com/specterops/bloodhound/crypto"
"github.com/specterops/bloodhound/errors"
"github.com/specterops/bloodhound/headers"
"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/src/auth"
Expand All @@ -45,12 +45,12 @@ import (
"github.com/specterops/bloodhound/src/model"
)

const (
ErrInvalidAuth = errors.Error("invalid authentication")
ErrNoUserSecret = errors.Error("user does not have a secret auth provider registered")
ErrUserDisabled = errors.Error("user disabled")
ErrorUserNotAuthorizedForProvider = errors.Error("user not authorized for this provider")
ErrorInvalidAuthProvider = errors.Error("invalid auth provider")
var (
ErrInvalidAuth = errors.New("invalid authentication")
ErrNoUserSecret = errors.New("user does not have a secret auth provider registered")
ErrUserDisabled = errors.New("user disabled")
ErrUserNotAuthorizedForProvider = errors.New("user not authorized for this provider")
ErrInvalidAuthProvider = errors.New("invalid auth provider")
)

func parseRequestDate(rawDate string) (time.Time, error) {
Expand Down Expand Up @@ -239,7 +239,7 @@ func (s authenticator) ValidateRequestSignature(tokenID uuid.UUID, request *http
} else if authContext, err := s.ctxInitializer.InitContextFromToken(request.Context(), authToken); err != nil {
return handleAuthDBError(err)
} else if user, isUser := auth.GetUserFromAuthCtx(authContext); isUser && user.IsDisabled {
return authContext, http.StatusForbidden, errors.Error("user disabled")
return authContext, http.StatusForbidden, ErrUserDisabled
} else if err := validateRequestTime(serverTime, requestDate); err != nil {
return auth.Context{}, http.StatusUnauthorized, err
} else {
Expand Down Expand Up @@ -384,7 +384,7 @@ func (s authenticator) CreateSSOSession(request *http.Request, response http.Res
}
} else {
if !user.SSOProviderID.Valid || ssoProvider.ID != user.SSOProviderID.Int32 {
auditLogFields["error"] = ErrorUserNotAuthorizedForProvider
auditLogFields["error"] = ErrUserNotAuthorizedForProvider
WriteErrorResponse(requestCtx, BuildErrorResponse(http.StatusForbidden, "user is not allowed", request), response)
return
}
Expand Down Expand Up @@ -436,7 +436,7 @@ func (s authenticator) CreateSession(ctx context.Context, user model.User, authP
userSession.AuthProviderType = model.SessionAuthProviderOIDC
userSession.AuthProviderID = typedAuthProvider.ID
default:
return "", ErrorInvalidAuthProvider
return "", ErrInvalidAuthProvider
}

if newSession, err := s.db.CreateUserSession(ctx, userSession); err != nil {
Expand Down
20 changes: 11 additions & 9 deletions cmd/api/src/api/marshalling.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"

"github.com/specterops/bloodhound/errors"
"github.com/specterops/bloodhound/headers"
"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/mediatypes"
Expand All @@ -36,9 +36,11 @@ import (
const (
// DefaultAPIPayloadReadLimitBytes sets the maximum API body size to 10MB
DefaultAPIPayloadReadLimitBytes = 10 * 1024 * 1024
)

ErrorContentTypeJson = errors.Error("content type must be application/json")
ErrorNoRequestBody = errors.Error("request body is empty")
var (
ErrContentTypeJson = errors.New("content type must be application/json")
ErrNoRequestBody = errors.New("request body is empty")
)

// These are the standardized API V2 response structures
Expand Down Expand Up @@ -190,7 +192,7 @@ func WriteBinaryResponse(_ context.Context, data []byte, filename string, status

func ReadJsonResponsePayload(value any, response *http.Response) error {
if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) {
return ErrorContentTypeJson
return ErrContentTypeJson
}

decoder := json.NewDecoder(response.Body)
Expand All @@ -203,7 +205,7 @@ func ReadJsonResponsePayload(value any, response *http.Response) error {

func ReadAPIV2ResponsePayload(value any, response *http.Response) error {
if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) {
return ErrorContentTypeJson
return ErrContentTypeJson
}

var wrapper BasicResponse
Expand All @@ -221,7 +223,7 @@ func ReadAPIV2ResponsePayload(value any, response *http.Response) error {

func ReadAPIV2ResponseWrapperPayload(value any, response *http.Response) error {
if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) {
return ErrorContentTypeJson
return ErrContentTypeJson
}

if content, err := io.ReadAll(response.Body); err != nil {
Expand All @@ -235,7 +237,7 @@ func ReadAPIV2ResponseWrapperPayload(value any, response *http.Response) error {

func ReadAPIV2ErrorResponsePayload(value *ErrorWrapper, response *http.Response) error {
if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) {
return ErrorContentTypeJson
return ErrContentTypeJson
}

if content, err := io.ReadAll(response.Body); err != nil {
Expand All @@ -249,11 +251,11 @@ func ReadAPIV2ErrorResponsePayload(value *ErrorWrapper, response *http.Response)

func ReadJSONRequestPayloadLimited(value any, request *http.Request) error {
if !utils.HeaderMatches(request.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) {
return ErrorContentTypeJson
return ErrContentTypeJson
}

if request.Body == nil {
return ErrorNoRequestBody
return ErrNoRequestBody
}

var (
Expand Down
2 changes: 1 addition & 1 deletion cmd/api/src/api/registration/registration.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func RegisterFossGlobalMiddleware(routerInst *router.Router, cfg config.Configur
func RegisterFossRoutes(
routerInst *router.Router,
cfg config.Configuration,
rdms *database.BloodhoundDB,
rdms database.Database,
graphDB *graph.DatabaseSwitch,
graphQuery queries.Graph,
apiCache cache.Cache,
Expand Down
10 changes: 5 additions & 5 deletions cmd/api/src/api/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
"github.com/specterops/bloodhound/headers"
)

const ErrorTemplateHMACSignature string = "unable to compute hmac signature: %w"
const ErrTemplateHMACSignature string = "unable to compute hmac signature: %w"

// tee takes a source reader and two writers. The function reads from the source until exhaustion. Each read is written
// serially to both writers.
Expand Down Expand Up @@ -138,7 +138,7 @@ func (s *SelfDestructingTempFile) Name() string {
// NOTE: The given io.Reader will be read to EOF. Consider using io.TeeReader so that the body may be read again after the signature has been created.
func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key string, datetime string, requestMethod string, requestURI string, body io.Reader) ([]byte, error) {
if hasher == nil {
return nil, fmt.Errorf(ErrorTemplateHMACSignature, fmt.Errorf("hasher must not be nil"))
return nil, fmt.Errorf(ErrTemplateHMACSignature, fmt.Errorf("hasher must not be nil"))
}

digester := hmac.New(hasher, []byte(key))
Expand All @@ -150,7 +150,7 @@ func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key strin
// Example: GET /api/v2/test/resource HTTP/1.1
// Signature Component: GET/api/v2/test/resource
if _, err := digester.Write([]byte(requestMethod + requestURI)); err != nil {
return nil, fmt.Errorf(ErrorTemplateHMACSignature, err)
return nil, fmt.Errorf(ErrTemplateHMACSignature, err)
}

// DateKey is the next HMAC digest link in the signature chain. This encodes the RFC3339 formatted datetime
Expand All @@ -163,7 +163,7 @@ func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key strin
digester = hmac.New(hasher, digester.Sum(nil))

if _, err := digester.Write([]byte(datetime[:13])); err != nil {
return nil, fmt.Errorf(ErrorTemplateHMACSignature, err)
return nil, fmt.Errorf(ErrTemplateHMACSignature, err)
}

// Body signing is the last HMAC digest link in the signature chain. This encodes the request body as part of
Expand All @@ -179,7 +179,7 @@ func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key strin

if body != nil {
if _, err := io.Copy(digester, body); err != nil {
return nil, fmt.Errorf(ErrorTemplateHMACSignature, err)
return nil, fmt.Errorf(ErrTemplateHMACSignature, err)
}
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/api/src/api/signature_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import (
"bytes"
"context"
"crypto/sha256"
"errors"
"io"
"strings"
"testing"
"testing/iotest"
"time"

"github.com/specterops/bloodhound/errors"
"github.com/stretchr/testify/require"
)

Expand Down
12 changes: 6 additions & 6 deletions cmd/api/src/api/tools/analysis_schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ type ScheduledAnalysisConfiguration struct {
RRule string `json:"rrule"`
}

const ErrorInvalidRrule = "invalid rrule specified: %v"
const ErrorFailedRetrievingData = "error retrieving configuration data: %v"
const ErrInvalidRrule = "invalid rrule specified: %v"
const ErrFailedRetrievingData = "error retrieving configuration data: %v"

func (s ToolContainer) GetScheduledAnalysisConfiguration(response http.ResponseWriter, request *http.Request) {
if config, err := appcfg.GetScheduledAnalysisParameter(request.Context(), s.db); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf(ErrorFailedRetrievingData, err), request), response)
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf(ErrFailedRetrievingData, err), request), response)
} else {
api.WriteJSONResponse(request.Context(), config, http.StatusOK, response)
}
Expand Down Expand Up @@ -70,11 +70,11 @@ func (s ToolContainer) SetScheduledAnalysisConfiguration(response http.ResponseW
//Validate that the rrule is a good rule. We're going to require a DTSTART to keep scheduling consistent.
//We're also going to reject UNTIL/COUNT because it will most likely break the pipeline once it's hit without being invalid
if _, err := rrule.StrToRRule(config.RRule); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRrule, err), request), response)
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrInvalidRrule, err), request), response)
} else if strings.Contains(strings.ToUpper(config.RRule), "UNTIL") || strings.Contains(strings.ToUpper(config.RRule), "COUNT") {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRrule, "count/until not supported"), request), response)
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrInvalidRrule, "count/until not supported"), request), response)
} else if !strings.Contains(strings.ToUpper(config.RRule), "DTSTART") {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRrule, "dtstart is required"), request), response)
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrInvalidRrule, "dtstart is required"), request), response)
} else {
nextParameter := appcfg.ScheduledAnalysisParameter{
Enabled: true,
Expand Down
Loading

0 comments on commit 59a0c4b

Please sign in to comment.