diff --git a/cmd/api/src/api/filters.go b/cmd/api/src/api/filters.go new file mode 100644 index 000000000..94a50b873 --- /dev/null +++ b/cmd/api/src/api/filters.go @@ -0,0 +1,57 @@ +// 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 api + +import ( + "errors" + "fmt" + + "github.com/specterops/bloodhound/src/model" +) + +var ( + ErrColumnUnfilterable = errors.New("the specified column cannot be filtered") + ErrFilterPredicateNotSupported = errors.New("the specified filter predicate is not supported for this column") + ErrNoFindingType = errors.New("no finding type specified") + ErrColumnFormatNotSupported = errors.New("column format does not support sorting") + ErrInvalidFindingType = errors.New("invalid finding type specified") + ErrInvalidAcceptedFilter = errors.New("invalid finding type specified") +) + +type Filterable interface { + ValidFilters() map[string][]model.FilterOperator +} + +func GetFilterableColumns(f Filterable) []string { + var columns = make([]string, 0) + for column := range f.ValidFilters() { + columns = append(columns, column) + } + return columns +} + +func GetValidFilterPredicatesAsStrings(f Filterable, column string) ([]string, error) { + if predicates, validColumn := f.ValidFilters()[column]; !validColumn { + return []string{}, fmt.Errorf("the specified column cannot be filtered") + } else { + var stringPredicates = make([]string, 0) + for _, predicate := range predicates { + stringPredicates = append(stringPredicates, string(predicate)) + } + return stringPredicates, nil + } +} diff --git a/cmd/api/src/api/v2/analysisrequest.go b/cmd/api/src/api/v2/analysisrequest.go index 63e779f2d..af6743d92 100644 --- a/cmd/api/src/api/v2/analysisrequest.go +++ b/cmd/api/src/api/v2/analysisrequest.go @@ -20,12 +20,12 @@ import ( "database/sql" "net/http" - "github.com/specterops/bloodhound/src/model/appcfg" "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" + "github.com/specterops/bloodhound/src/model/appcfg" ) const ErrAnalysisScheduledMode = "analysis is configured to run on a schedule, unable to run just in time" diff --git a/cmd/api/src/model/appcfg/parameter.go b/cmd/api/src/model/appcfg/parameter.go index fbf69ce9d..e4d63a646 100644 --- a/cmd/api/src/model/appcfg/parameter.go +++ b/cmd/api/src/model/appcfg/parameter.go @@ -163,6 +163,7 @@ func (s *PasswordExpiration) UnmarshalJSON(data []byte) error { return nil } + } func GetPasswordExpiration(ctx context.Context, service ParameterService) time.Duration { diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 2c84504df..3eabaf1b5 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -12166,7 +12166,7 @@ "get": { "operationId": "ListDomainAttackPathsDetails", "summary": "List domain attack paths details", - "description": "Lists detailed data about attack paths for a domain.", + "description": "Lists detailed data about attack paths for a domain. \n\n__Note:__ __Note:__ `ImpactCount`, `ImpactPercentage`, `ExposureCount`, and `ExposurePercentage` will have a value other than zero when butterfly analysis is enabled.\n", "tags": [ "Attack Paths", "Enterprise" @@ -12174,7 +12174,6 @@ "parameters": [ { "name": "finding", - "x-go-name": "FindingDeprecated", "in": "query", "schema": { "$ref": "#/components/schemas/api.params.predicate.filter.string" @@ -12182,7 +12181,7 @@ }, { "name": "sort_by", - "description": "Sortable columns are `domain_sid`, `index`, `AcceptedUntil`, `id`, `created_at`, `updated_at`, `deleted_at`. Relationship risks can be sorted on `FromPrincipal` and `ToPrincipal` in addition to the sortable columns for List Risks.", + "description": "Sortable columns are `domain_sid`, `index`, `AcceptedUntil`, `id`, `created_at`, `updated_at`, `deleted_at`, `exposure_percent`, `impact_percent`. Relationship risks can be sorted on `FromPrincipal` and `ToPrincipal` in addition to the sortable columns for List Risks.", "in": "query", "schema": { "$ref": "#/components/schemas/api.params.query.sort-by" @@ -12191,7 +12190,6 @@ { "name": "FromPrincipal", "deprecated": true, - "x-go-name": "FromPrincipalDeprecated", "in": "query", "schema": { "$ref": "#/components/schemas/api.params.predicate.filter.string" @@ -12200,7 +12198,6 @@ { "name": "ToPrincipal", "deprecated": true, - "x-go-name": "ToPrincipalDeprecated", "in": "query", "schema": { "$ref": "#/components/schemas/api.params.predicate.filter.string" @@ -12237,7 +12234,6 @@ { "name": "AcceptedUntil", "deprecated": true, - "x-go-name": "AcceptedUntilDeprecated", "in": "query", "schema": { "$ref": "#/components/schemas/api.params.predicate.filter.time" @@ -12259,6 +12255,7 @@ }, { "name": "Finding", + "deprecated": true, "in": "query", "schema": { "$ref": "#/components/schemas/api.params.predicate.filter.string" @@ -12362,6 +12359,186 @@ ] } ] + }, + "examples": { + "Butterfly Relationship Finding": { + "summary": "Butterfly Relationship Finding", + "description": "When the butterfly analysis feature flag is on, impact count/percentage and exposure count/percentage will have a value other than zero.", + "value": { + "count": 0, + "skip": 0, + "limit": 0, + "data": [ + { + "id": 0, + "created_at": "2024-08-28T21:21:40.845Z", + "updated_at": "2024-08-28T21:21:40.845Z", + "deleted_at": { + "time": "2024-08-28T21:21:40.845Z", + "valid": true + }, + "FromPrincipal": "string", + "ToPrincipal": "string", + "FromPrincipalProps": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "FromPrincipalKind": "string", + "ToPrincipalProps": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "ToPrincipalKind": "string", + "RelProps": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "ComboGraphRelationID": { + "int64": 0, + "valid": true + }, + "Finding": "string", + "DomainSID": "string", + "PrincipalHash": "string", + "AcceptedUntil": "2024-08-28T21:21:40.845Z", + "ImpactPercentage": 12, + "ImpactCount": 2, + "ExposurePercentage": 24, + "ExposureCount": 4, + "Severity": "high", + "Accepted": true + } + ] + } + }, + "Metatree Relationship Finding": { + "summary": "Metatree Relationship Finding", + "description": "When the butterfly analysis feature flag is off and metatree is running, impact count/percentage and exposure count/percentage will have a value of zero.", + "value": { + "count": 0, + "skip": 0, + "limit": 0, + "data": [ + { + "id": 0, + "created_at": "2024-08-28T21:21:40.845Z", + "updated_at": "2024-08-28T21:21:40.845Z", + "deleted_at": { + "time": "2024-08-28T21:21:40.845Z", + "valid": true + }, + "FromPrincipal": "string", + "ToPrincipal": "string", + "FromPrincipalProps": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "FromPrincipalKind": "string", + "ToPrincipalProps": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "ToPrincipalKind": "string", + "RelProps": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "ComboGraphRelationID": { + "int64": 0, + "valid": true + }, + "Finding": "string", + "DomainSID": "string", + "PrincipalHash": "string", + "AcceptedUntil": "2024-08-28T21:21:40.845Z", + "ImpactPercentage": 0, + "ImpactCount": 0, + "ExposurePercentage": 0, + "ExposureCount": 0, + "Severity": "", + "Accepted": true + } + ] + } + }, + "Butterfly List Finding": { + "summary": "Butterfly List Finding", + "description": "When the butterfly analysis feature flag is on, impact count/percentage and exposure count/percentage will have a value other than zero.", + "value": { + "count": "0,", + "skip": "0,", + "limit": "0,", + "data": [ + { + "id": 0, + "created_at": "2024-08-28T21:42:18.844Z", + "updated_at": "2024-08-28T21:42:18.844Z", + "deleted_at": { + "time": "2024-08-28T21:42:18.844Z", + "valid": true + }, + "Principal": "string", + "PrincipalKind": "string", + "Finding": "string", + "DomainSID": "string", + "Props": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "accepted_until": "2024-08-28T21:42:18.844Z", + "ImpactPercentage": 12, + "ImpactCount": 2, + "ExposurePercentage": 24, + "ExposureCount": 4, + "Severity": "high", + "Accepted": true + } + ] + } + }, + "Metatree List Finding": { + "summary": "Metatree List Finding", + "description": "When the butterfly analysis feature flag is off and metatree is running, impact count/percentage and exposure count/percentage will have a value of zero.", + "value": { + "count": "0,", + "skip": "0,", + "limit": "0,", + "data": [ + { + "id": 0, + "created_at": "2024-08-28T21:42:18.844Z", + "updated_at": "2024-08-28T21:42:18.844Z", + "deleted_at": { + "time": "2024-08-28T21:42:18.844Z", + "valid": true + }, + "Principal": "string", + "PrincipalKind": "string", + "Finding": "string", + "DomainSID": "string", + "Props": { + "additionalProp1": {}, + "additionalProp2": {}, + "additionalProp3": {} + }, + "accepted_until": "2024-08-28T21:42:18.844Z", + "ImpactPercentage": 0, + "ImpactCount": 0, + "ExposurePercentage": 0, + "ExposureCount": 0, + "Severity": "", + "Accepted": true + } + ] + } + } } } } @@ -15399,6 +15576,32 @@ "AcceptedUntil": { "type": "string", "format": "date-time" + }, + "ImpactPercentage": { + "type": "number", + "format": "double" + }, + "ImpactCount": { + "type": "integer", + "format": "int64" + }, + "ExposurePercentage": { + "type": "number", + "format": "double" + }, + "ExposureCount": { + "type": "integer", + "format": "int64" + }, + "Severity": { + "type": "string", + "enum": [ + "critical", + "high", + "moderate", + "low", + "" + ] } } } @@ -15436,6 +15639,32 @@ "accepted_until": { "type": "string", "format": "date-time" + }, + "ImpactPercentage": { + "type": "number", + "format": "double" + }, + "ImpactCount": { + "type": "integer", + "format": "int64" + }, + "ExposurePercentage": { + "type": "number", + "format": "double" + }, + "ExposureCount": { + "type": "integer", + "format": "int64" + }, + "Severity": { + "type": "string", + "enum": [ + "critical", + "high", + "moderate", + "low", + "" + ] } } } diff --git a/packages/go/openapi/src/paths/attack-paths.domains.id.details.yaml b/packages/go/openapi/src/paths/attack-paths.domains.id.details.yaml index 8cfb405bb..46657a116 100644 --- a/packages/go/openapi/src/paths/attack-paths.domains.id.details.yaml +++ b/packages/go/openapi/src/paths/attack-paths.domains.id.details.yaml @@ -25,19 +25,21 @@ parameters: get: operationId: ListDomainAttackPathsDetails summary: List domain attack paths details - description: Lists detailed data about attack paths for a domain. + description: | + Lists detailed data about attack paths for a domain. + + __Note:__ __Note:__ `ImpactCount`, `ImpactPercentage`, `ExposureCount`, and `ExposurePercentage` will have a value other than zero when butterfly analysis is enabled. tags: - Attack Paths - Enterprise parameters: - name: finding - x-go-name: "FindingDeprecated" in: query schema: $ref: './../schemas/api.params.predicate.filter.string.yaml' - name: sort_by description: Sortable columns are `domain_sid`, `index`, `AcceptedUntil`, - `id`, `created_at`, `updated_at`, `deleted_at`. Relationship risks can be sorted on + `id`, `created_at`, `updated_at`, `deleted_at`, `exposure_percent`, `impact_percent`. Relationship risks can be sorted on `FromPrincipal` and `ToPrincipal` in addition to the sortable columns for List Risks. in: query @@ -45,13 +47,11 @@ get: $ref: './../schemas/api.params.query.sort-by.yaml' - name: FromPrincipal deprecated: true - x-go-name: "FromPrincipalDeprecated" in: query schema: $ref: './../schemas/api.params.predicate.filter.string.yaml' - name: ToPrincipal deprecated: true - x-go-name: "ToPrincipalDeprecated" in: query schema: $ref: './../schemas/api.params.predicate.filter.string.yaml' @@ -73,7 +73,6 @@ get: $ref: './../schemas/api.params.predicate.filter.string.yaml' - name: AcceptedUntil deprecated: true - x-go-name: "AcceptedUntilDeprecated" in: query schema: $ref: './../schemas/api.params.predicate.filter.time.yaml' @@ -86,6 +85,7 @@ get: schema: $ref: './../schemas/api.params.predicate.filter.string.yaml' - name: Finding + deprecated: true in: query schema: $ref: './../schemas/api.params.predicate.filter.string.yaml' @@ -135,6 +135,178 @@ get: properties: Accepted: type: boolean + # !!!!! New examples !!!!! + examples: + Butterfly Relationship Finding: + summary: "Butterfly Relationship Finding" + description: "When the butterfly analysis feature flag is on, impact count/percentage and exposure count/percentage will have a value other than zero." + value: + count: 0 + skip: 0 + limit: 0 + data: [ + { + id: 0, + created_at: "2024-08-28T21:21:40.845Z", + updated_at: "2024-08-28T21:21:40.845Z", + deleted_at: { + time: "2024-08-28T21:21:40.845Z", + valid: true + }, + FromPrincipal: string, + ToPrincipal: string, + FromPrincipalProps: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + FromPrincipalKind: string, + ToPrincipalProps: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + ToPrincipalKind: string, + RelProps: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + ComboGraphRelationID: { + int64: 0, + valid: true + }, + Finding: string, + DomainSID: string, + PrincipalHash: string, + AcceptedUntil: "2024-08-28T21:21:40.845Z", + ImpactPercentage: 12, + ImpactCount: 2, + ExposurePercentage: 24, + ExposureCount: 4, + Severity: 'high', + Accepted: true, + } + ] + Metatree Relationship Finding: + summary: "Metatree Relationship Finding" + description: "When the butterfly analysis feature flag is off and metatree is running, impact count/percentage and exposure count/percentage will have a value of zero." + value: + count: 0 + skip: 0 + limit: 0 + data: [ + { + id: 0, + created_at: "2024-08-28T21:21:40.845Z", + updated_at: "2024-08-28T21:21:40.845Z", + deleted_at: { + time: "2024-08-28T21:21:40.845Z", + valid: true + }, + FromPrincipal: string, + ToPrincipal: string, + FromPrincipalProps: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + FromPrincipalKind: string, + ToPrincipalProps: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + ToPrincipalKind: string, + RelProps: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + ComboGraphRelationID: { + int64: 0, + valid: true + }, + Finding: string, + DomainSID: string, + PrincipalHash: string, + AcceptedUntil: "2024-08-28T21:21:40.845Z", + ImpactPercentage: 0, + ImpactCount: 0, + ExposurePercentage: 0, + ExposureCount: 0, + Severity: '', + Accepted: true + } + ] + Butterfly List Finding: + summary: "Butterfly List Finding" + description: "When the butterfly analysis feature flag is on, impact count/percentage and exposure count/percentage will have a value other than zero." + value: + count: 0, + skip: 0, + limit: 0, + data: [ + { + id: 0, + created_at: "2024-08-28T21:42:18.844Z", + updated_at: "2024-08-28T21:42:18.844Z", + deleted_at: { + time: "2024-08-28T21:42:18.844Z", + valid: true + }, + Principal: string, + PrincipalKind: string, + Finding: string, + DomainSID: string, + Props: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + accepted_until: "2024-08-28T21:42:18.844Z", + ImpactPercentage: 12, + ImpactCount: 2, + ExposurePercentage: 24, + ExposureCount: 4, + Severity: 'high', + Accepted: true + } + ] + Metatree List Finding: + summary: "Metatree List Finding" + description: "When the butterfly analysis feature flag is off and metatree is running, impact count/percentage and exposure count/percentage will have a value of zero." + value: + count: 0, + skip: 0, + limit: 0, + data: [ + { + id: 0, + created_at: "2024-08-28T21:42:18.844Z", + updated_at: "2024-08-28T21:42:18.844Z", + deleted_at: { + time: "2024-08-28T21:42:18.844Z", + valid: true + }, + Principal: string, + PrincipalKind: string, + Finding: string, + DomainSID: string, + Props: { + additionalProp1: {}, + additionalProp2: {}, + additionalProp3: {} + }, + accepted_until: "2024-08-28T21:42:18.844Z", + ImpactPercentage: 0, + ImpactCount: 0, + ExposurePercentage: 0, + ExposureCount: 0, + Severity: '', + Accepted: true + } + ] 400: $ref: './../responses/bad-request.yaml' 401: diff --git a/packages/go/openapi/src/schemas/model.list-finding.yaml b/packages/go/openapi/src/schemas/model.list-finding.yaml index b00ff5405..f28376889 100644 --- a/packages/go/openapi/src/schemas/model.list-finding.yaml +++ b/packages/go/openapi/src/schemas/model.list-finding.yaml @@ -34,3 +34,23 @@ allOf: accepted_until: type: string format: date-time + ImpactPercentage: + type: number + format: double + ImpactCount: + type: integer + format: int64 + ExposurePercentage: + type: number + format: double + ExposureCount: + type: integer + format: int64 + Severity: + type: string + enum: + - critical + - high + - moderate + - low + - "" \ No newline at end of file diff --git a/packages/go/openapi/src/schemas/model.relationship-finding.yaml b/packages/go/openapi/src/schemas/model.relationship-finding.yaml index 65cef89a5..15b306262 100644 --- a/packages/go/openapi/src/schemas/model.relationship-finding.yaml +++ b/packages/go/openapi/src/schemas/model.relationship-finding.yaml @@ -50,3 +50,23 @@ allOf: AcceptedUntil: type: string format: date-time + ImpactPercentage: + type: number + format: double + ImpactCount: + type: integer + format: int64 + ExposurePercentage: + type: number + format: double + ExposureCount: + type: integer + format: int64 + Severity: + type: string + enum: + - critical + - high + - moderate + - low + - "" \ No newline at end of file