Skip to content

Commit f6dc6be

Browse files
authored
Merge pull request #12 from TheManticoreProject/bugfix-property-value-validation
Fix property validation accepting null and non-primitive arrays (#11)
2 parents 2c761fa + c68604e commit f6dc6be

2 files changed

Lines changed: 68 additions & 15 deletions

File tree

properties/Properties.go

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,66 @@ func (p *Properties) Clear() {
7070
p.Properties = make(map[string]interface{})
7171
}
7272

73-
// isValidPropertyValue checks if a value is a valid property type
73+
// IsPropertyValueValid reports whether value is a valid OpenGraph property value.
74+
//
75+
// The BloodHound OpenGraph schema restricts a property value to a single
76+
// primitive (string, number, or boolean) or a homogeneous array of primitives.
77+
// null values, nested objects, arrays of objects, and arrays mixing primitive
78+
// types are not valid.
79+
//
80+
// Source: https://bloodhound.specterops.io/opengraph/developer/nodes
7481
func (p *Properties) IsPropertyValueValid(value interface{}) bool {
7582
if value == nil {
76-
return true
83+
return false
84+
}
85+
86+
switch reflect.TypeOf(value).Kind() {
87+
case reflect.Slice, reflect.Array:
88+
return isHomogeneousPrimitiveSequence(reflect.ValueOf(value))
89+
default:
90+
return primitiveCategory(value) != ""
91+
}
92+
}
93+
94+
// primitiveCategory classifies value into one of the OpenGraph primitive
95+
// categories ("string", "number", "boolean"). It returns "" when value is nil
96+
// or not a primitive (e.g. an object, slice, or array).
97+
func primitiveCategory(value interface{}) string {
98+
if value == nil {
99+
return ""
77100
}
78101

79-
valueType := reflect.TypeOf(value)
80-
switch valueType.Kind() {
81-
case reflect.String, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
102+
switch reflect.TypeOf(value).Kind() {
103+
case reflect.String:
104+
return "string"
105+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
82106
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
83-
reflect.Float32, reflect.Float64, reflect.Bool, reflect.Slice:
84-
return true
107+
reflect.Float32, reflect.Float64:
108+
return "number"
109+
case reflect.Bool:
110+
return "boolean"
85111
default:
86-
return false
112+
return ""
113+
}
114+
}
115+
116+
// isHomogeneousPrimitiveSequence reports whether v is a slice or array whose
117+
// elements are all primitives belonging to the same OpenGraph category. An
118+
// empty sequence is considered valid.
119+
func isHomogeneousPrimitiveSequence(v reflect.Value) bool {
120+
category := ""
121+
for i := 0; i < v.Len(); i++ {
122+
c := primitiveCategory(v.Index(i).Interface())
123+
if c == "" {
124+
return false
125+
}
126+
if category == "" {
127+
category = c
128+
} else if c != category {
129+
return false
130+
}
87131
}
132+
return true
88133
}
89134

90135
// ToDict converts properties to map for JSON serialization

properties/Properties_test.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ func TestSetProperty(t *testing.T) {
4646
{"int", 42},
4747
{"float", 3.14},
4848
{"bool", true},
49-
{"nil", nil},
50-
{"slice", []string{"a", "b", "c"}},
49+
{"string-slice", []string{"a", "b", "c"}},
50+
{"int-slice", []int{1, 2, 3}},
5151
}
5252

5353
for _, test := range validTests {
@@ -69,9 +69,12 @@ func TestSetProperty(t *testing.T) {
6969

7070
// Test invalid property types (should panic)
7171
invalidTests := []interface{}{
72-
map[string]string{"key": "value"}, // map
73-
struct{}{}, // struct
74-
func() {}, // function
72+
nil, // null is not an allowed property value
73+
map[string]string{"key": "value"}, // map / nested object
74+
struct{}{}, // struct
75+
func() {}, // function
76+
[]map[string]string{{"key": "value"}}, // array of objects
77+
[]interface{}{1, "a"}, // array mixing primitive types
7578
}
7679

7780
for _, invalidValue := range invalidTests {
@@ -260,9 +263,10 @@ func TestIsValidPropertyValue(t *testing.T) {
260263
42,
261264
3.14,
262265
true,
263-
nil,
264266
[]string{"a", "b"},
265267
[]int{1, 2, 3},
268+
[]bool{true, false},
269+
[]string{}, // empty homogeneous array
266270
}
267271

268272
for _, value := range validTypes {
@@ -273,10 +277,14 @@ func TestIsValidPropertyValue(t *testing.T) {
273277

274278
// Test invalid types
275279
invalidTypes := []interface{}{
276-
map[string]string{"key": "value"},
280+
nil, // null is not an allowed property value
281+
map[string]string{"key": "value"}, // nested object
277282
struct{ field string }{field: "value"},
278283
func() {},
279284
make(chan int),
285+
[]map[string]string{{"key": "value"}}, // array of objects
286+
[]interface{}{1, "a"}, // array mixing primitive types
287+
[][]int{{1, 2}}, // array of arrays
280288
}
281289

282290
for _, value := range invalidTypes {

0 commit comments

Comments
 (0)