diff --git a/cli/printer/mql.go b/cli/printer/mql.go index 2421b8942a..0bec80072d 100644 --- a/cli/printer/mql.go +++ b/cli/printer/mql.go @@ -694,6 +694,9 @@ func (print *Printer) Data(typ types.Type, data interface{}, codeID string, bund case types.Block: return print.refMap(typ, data.(map[string]interface{}), codeID, bundle, indent) + case types.Semver: + return print.Secondary(data.(string)) + case types.ArrayLike: if data == nil { return print.Secondary("null") diff --git a/llx/builtin.go b/llx/builtin.go index 6f37957089..169fbd1ea2 100644 --- a/llx/builtin.go +++ b/llx/builtin.go @@ -267,6 +267,8 @@ func init() { string("!=" + types.Float): {f: stringNotFloatV2, Label: "!="}, string("==" + types.Dict): {f: stringCmpDictV2, Label: "=="}, string("!=" + types.Dict): {f: stringNotDictV2, Label: "!="}, + string("==" + types.Semver): {f: semverCmpSemver, Label: "=="}, + string("!=" + types.Semver): {f: semverNotSemver, Label: "!="}, string("==" + types.ArrayLike): {f: chunkEqFalseV2, Label: "=="}, string("!=" + types.ArrayLike): {f: chunkNeqTrueV2, Label: "!="}, string("==" + types.Array(types.String)): {f: stringCmpStringarrayV2, Label: "=="}, @@ -293,6 +295,10 @@ func init() { string("<=" + types.Dict): {f: stringLTEDictV2, Label: "<="}, string(">" + types.Dict): {f: stringGTDictV2, Label: ">"}, string(">=" + types.Dict): {f: stringGTEDictV2, Label: ">="}, + string("<" + types.Semver): {f: semverLTsemver, Label: "<"}, + string(">" + types.Semver): {f: semverGTsemver, Label: ">"}, + string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="}, + string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="}, string("&&" + types.Bool): {f: stringAndBoolV2, Label: "&&"}, string("||" + types.Bool): {f: stringOrBoolV2, Label: "||"}, string("&&" + types.Int): {f: stringAndIntV2, Label: "&&"}, @@ -453,6 +459,8 @@ func init() { string("!=" + types.String): {f: dictNotStringV2, Label: "!="}, string("==" + types.Regex): {f: dictCmpRegexV2, Label: "=="}, string("!=" + types.Regex): {f: dictNotRegexV2, Label: "!="}, + string("==" + types.Semver): {f: semverCmpSemver, Label: "=="}, + string("!=" + types.Semver): {f: semverNotSemver, Label: "!="}, string("==" + types.ArrayLike): {f: dictCmpArrayV2, Label: "=="}, string("!=" + types.ArrayLike): {f: dictNotArrayV2, Label: "!="}, string("==" + types.Array(types.String)): {f: dictCmpStringarrayV2, Label: "=="}, @@ -481,6 +489,10 @@ func init() { string("<=" + types.Dict): {f: dictLTEDictV2, Label: "<="}, string(">" + types.Dict): {f: dictGTDictV2, Label: ">"}, string(">=" + types.Dict): {f: dictGTEDictV2, Label: ">="}, + string("<" + types.Semver): {f: semverLTsemver, Label: "<"}, + string(">" + types.Semver): {f: semverGTsemver, Label: ">"}, + string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="}, + string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="}, string("&&" + types.Bool): {f: dictAndBoolV2, Label: "&&"}, string("||" + types.Bool): {f: dictOrBoolV2, Label: "||"}, string("&&" + types.Int): {f: dictAndIntV2, Label: "&&"}, @@ -547,6 +559,24 @@ func init() { // We have not yet decided if and how these may be exposed to users "notEmpty": {f: dictNotEmptyV2}, }, + types.Semver: { + string("==" + types.Nil): {f: stringCmpNilV2, Label: "=="}, + string("!=" + types.Nil): {f: stringNotNilV2, Label: "!="}, + string("==" + types.Empty): {f: stringCmpEmptyV2, Label: "=="}, + string("!=" + types.Empty): {f: stringNotEmptyV2, Label: "!="}, + string("==" + types.Semver): {f: semverCmpSemver, Label: "=="}, + string("!=" + types.Semver): {f: semverNotSemver, Label: "!="}, + string("<" + types.Semver): {f: semverLTsemver, Label: "<"}, + string(">" + types.Semver): {f: semverGTsemver, Label: ">"}, + string("<=" + types.Semver): {f: semverLTEsemver, Label: "<="}, + string(">=" + types.Semver): {f: semverGTEsemver, Label: ">="}, + string("==" + types.String): {f: semverCmpSemver, Label: "=="}, + string("!=" + types.String): {f: semverNotSemver, Label: "!="}, + string("<" + types.String): {f: semverLTsemver, Label: "<"}, + string(">" + types.String): {f: semverGTsemver, Label: ">"}, + string("<=" + types.String): {f: semverLTEsemver, Label: "<="}, + string(">=" + types.String): {f: semverGTEsemver, Label: ">="}, + }, types.ArrayLike: { "[]": {f: arrayGetIndexV2}, "first": {f: arrayGetFirstIndexV2}, diff --git a/llx/builtin_global.go b/llx/builtin_global.go index 1b12185510..37280bc6a2 100644 --- a/llx/builtin_global.go +++ b/llx/builtin_global.go @@ -34,6 +34,7 @@ func init() { "switch": switchCallV2, "score": scoreCallV2, "typeof": typeofCallV2, + "semver": semverCall, "{}": blockV2, "return": returnCallV2, "createResource": globalCreateResource, @@ -182,6 +183,19 @@ func typeofCallV2(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, return StringData(res.Type.Label()), 0, nil } +func semverCall(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, error) { + if len(f.Args) != 1 { + return nil, 0, errors.New("Called `semver` with " + strconv.Itoa(len(f.Args)) + " arguments, expected one") + } + + res, dref, err := e.resolveValue(f.Args[0], ref) + if err != nil || dref != 0 || res == nil { + return res, dref, err + } + + return &RawData{Type: types.Semver, Value: res.Value}, 0, nil +} + func expectV2(e *blockExecutor, f *Function, ref uint64) (*RawData, uint64, error) { if len(f.Args) != 1 { return nil, 0, errors.New("Called expect with " + strconv.Itoa(len(f.Args)) + " arguments, expected 1") diff --git a/llx/builtin_semver.go b/llx/builtin_semver.go new file mode 100644 index 0000000000..1b42a783dd --- /dev/null +++ b/llx/builtin_semver.go @@ -0,0 +1,85 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package llx + +import ( + "github.com/Masterminds/semver" + "go.mondoo.com/cnquery/v9/types" +) + +func semverLT(left interface{}, right interface{}) *RawData { + leftv, err := semver.NewVersion(left.(string)) + if err != nil { + return BoolData(left.(string) < right.(string)) + } + rightv, err := semver.NewVersion(right.(string)) + if err != nil { + return BoolData(left.(string) < right.(string)) + } + return BoolData(leftv.LessThan(rightv)) +} + +func semverGT(left interface{}, right interface{}) *RawData { + leftv, err := semver.NewVersion(left.(string)) + if err != nil { + return BoolData(left.(string) > right.(string)) + } + rightv, err := semver.NewVersion(right.(string)) + if err != nil { + return BoolData(left.(string) > right.(string)) + } + return BoolData(leftv.GreaterThan(rightv)) +} + +func semverLTE(left interface{}, right interface{}) *RawData { + leftv, err := semver.NewVersion(left.(string)) + if err != nil { + return BoolData(left.(string) <= right.(string)) + } + rightv, err := semver.NewVersion(right.(string)) + if err != nil { + return BoolData(left.(string) <= right.(string)) + } + return BoolData(!leftv.GreaterThan(rightv)) +} + +func semverGTE(left interface{}, right interface{}) *RawData { + leftv, err := semver.NewVersion(left.(string)) + if err != nil { + return BoolData(left.(string) >= right.(string)) + } + rightv, err := semver.NewVersion(right.(string)) + if err != nil { + return BoolData(left.(string) >= right.(string)) + } + return BoolData(!leftv.LessThan(rightv)) +} + +func semverCmpSemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) { + return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData { + return BoolData(left.(string) == right.(string)) + }) +} + +func semverNotSemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) { + return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, func(left, right interface{}) *RawData { + return BoolData(left.(string) != right.(string)) + }) +} + +func semverLTsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) { + return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverLT) +} + +func semverGTsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) { + return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverGT) +} + +func semverLTEsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) { + return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverLTE) +} + +func semverGTEsemver(e *blockExecutor, bind *RawData, chunk *Chunk, ref uint64) (*RawData, uint64, error) { + return nonNilDataOpV2(e, bind, chunk, ref, types.Bool, semverGTE) +} diff --git a/llx/data_conversions.go b/llx/data_conversions.go index f29266edf2..1d4cd8a07d 100644 --- a/llx/data_conversions.go +++ b/llx/data_conversions.go @@ -39,6 +39,7 @@ func init() { types.Score: score2result, types.Empty: empty2result, types.Block: block2result, + types.Semver: string2result, types.ArrayLike: array2result, types.MapLike: map2result, types.ResourceLike: resource2result, @@ -58,6 +59,7 @@ func init() { types.Score: pscore2raw, types.Empty: pempty2raw, types.Block: pblock2rawV2, + types.Semver: pscore2raw, types.ArrayLike: parray2raw, types.MapLike: pmap2raw, types.ResourceLike: presource2raw, diff --git a/mqlc/operators.go b/mqlc/operators.go index 0146252c2f..fa2b1d7c40 100644 --- a/mqlc/operators.go +++ b/mqlc/operators.go @@ -43,6 +43,7 @@ func init() { "switch": compileSwitch, "Never": compileNever, "empty": compileEmpty, + "semver": compileSemver, } } @@ -496,6 +497,33 @@ func compileTypeof(c *compiler, id string, call *parser.Call) (types.Type, error return types.String, nil } +func compileSemver(c *compiler, id string, call *parser.Call) (types.Type, error) { + if call == nil || len(call.Function) < 1 { + return types.Nil, errors.New("missing parameter for '" + id + "', it requires 1") + } + + arg := call.Function[0] + if arg == nil || arg.Value == nil || arg.Value.Operand == nil || arg.Value.Operand.Value == nil { + return types.Nil, errors.New("failed to get parameter for '" + id + "'") + } + + argValue, err := c.compileExpression(arg.Value) + if err != nil { + return types.Nil, err + } + + c.addChunk(&llx.Chunk{ + Call: llx.Chunk_FUNCTION, + Id: "semver", + Function: &llx.Function{ + Type: string(types.String), + Args: []*llx.Primitive{argValue}, + }, + }) + + return types.String, nil +} + func compileSwitch(c *compiler, id string, call *parser.Call) (types.Type, error) { var ref *llx.Primitive diff --git a/providers/core/resources/mql_test.go b/providers/core/resources/mql_test.go index 0d006847fd..dd046f1bb3 100644 --- a/providers/core/resources/mql_test.go +++ b/providers/core/resources/mql_test.go @@ -788,6 +788,31 @@ func TestTime(t *testing.T) { }) } +func TestSemver(t *testing.T) { + x.TestSimple(t, []testutils.SimpleTest{ + { + Code: "semver('1.2.3') == semver('1.2.3')", + ResultIndex: 2, Expectation: true, + }, + { + Code: "semver('1.2.3') == semver('1.2')", + ResultIndex: 2, Expectation: false, + }, + { + Code: "semver('1.2') < semver('1.10.2')", + ResultIndex: 2, Expectation: true, + }, + { + Code: "semver('1.10') >= semver('1.2.3')", + ResultIndex: 2, Expectation: true, + }, + { + Code: "semver('1.10') >= '1.2'", + ResultIndex: 2, Expectation: true, + }, + }) +} + func TestResource_Default(t *testing.T) { x := testutils.InitTester(testutils.LinuxMock()) res := x.TestQuery(t, "mondoo") diff --git a/types/types.go b/types/types.go index 001a44e143..6b0079456d 100644 --- a/types/types.go +++ b/types/types.go @@ -47,7 +47,8 @@ const ( byteScore byteBlock byteEmpty - byteArray = 1<<4 + iota - 5 // set to 25 to avoid breaking changes + byteSemver + byteArray = 1<<4 + iota - 6 // set to 25 to avoid breaking changes byteMap byteResource byteFunction @@ -87,6 +88,8 @@ const ( Block = Type(rune(byteBlock)) // Empty value Empty = Type(rune(byteEmpty)) + // Semver value + Semver = Type(rune(byteSemver)) // ArrayLike is the underlying type of all arrays ArrayLike = Type(rune(byteArray)) // MapLike is the underlying type of all maps @@ -255,6 +258,7 @@ var labels = map[byte]string{ byteScore: "score", byteBlock: "block", byteEmpty: "empty", + byteSemver: "semver", byteStringSlice: "stringslice", byteRange: "range", }