diff --git a/libpromises/evalfunction.c b/libpromises/evalfunction.c index d42df8c6fca..7f693714935 100644 --- a/libpromises/evalfunction.c +++ b/libpromises/evalfunction.c @@ -6571,6 +6571,48 @@ static FnCallResult FnCallIsIpInSubnet(ARG_UNUSED EvalContext *ctx, Log(LOG_LEVEL_DEBUG, "%s(%s): no match", fp->name, range); return FnReturnContext(false); } + +/*********************************************************************/ + +static FnCallResult FnCallIsValidIP(ARG_UNUSED EvalContext *ctx, + ARG_UNUSED const Policy *policy, + const FnCall *fp, const Rlist *finalargs) +{ + assert(fp != NULL); + + if (finalargs == NULL || + (finalargs->next != NULL && finalargs->next->next != NULL)) + { + Log(LOG_LEVEL_ERR, "Function '%s' requires one or two arguments", fp->name); + return FnFailure(); + } + + const char *address = RlistScalarValue(finalargs); + const char *version = (finalargs->next != NULL) + ? RlistScalarValue(finalargs->next) + : NULL; + + if (version != NULL && + !StringEqual(version, "ipv4") && !StringEqual(version, "ipv6")) + { + Log(LOG_LEVEL_ERR, + "Function '%s': invalid IP version '%s', expected 'ipv4' or 'ipv6'", + fp->name, version); + return FnFailure(); + } + + const bool check_ipv4 = (version == NULL || StringEqual(version, "ipv4")); + const bool check_ipv6 = (version == NULL || StringEqual(version, "ipv6")); + + unsigned char binary[sizeof(struct in6_addr)]; + const bool valid = + (check_ipv4 && inet_pton(AF_INET, address, binary) == 1) || + (check_ipv6 && inet_pton(AF_INET6, address, binary) == 1); + + Log(LOG_LEVEL_DEBUG, "%s(%s): %s", fp->name, address, valid ? "valid" : "invalid"); + return FnReturnContext(valid); +} + /*********************************************************************/ static FnCallResult FnCallHostRange(ARG_UNUSED EvalContext *ctx, ARG_UNUSED const Policy *policy, ARG_UNUSED const FnCall *fp, const Rlist *finalargs) @@ -11098,6 +11140,13 @@ static const FnCallArg IPRANGE_ARGS[] = {NULL, CF_DATA_TYPE_NONE, NULL} }; +static const FnCallArg ISVALIDIP_ARGS[] = +{ + {CF_ANYSTRING, CF_DATA_TYPE_STRING, "String to validate as an IP address"}, + {"ipv4,ipv6", CF_DATA_TYPE_OPTION, "Only accept this IP version"}, + {NULL, CF_DATA_TYPE_NONE, NULL} +}; + static const FnCallArg IRANGE_ARGS[] = { {CF_INTRANGE, CF_DATA_TYPE_INT, "Integer start of range"}, @@ -12004,6 +12053,8 @@ const FnCallType CF_FNCALL_TYPES[] = FNCALL_OPTION_VARARG, FNCALL_CATEGORY_COMM, SYNTAX_STATUS_NORMAL, ARGC(1, -1)), FnCallTypeNew("isipinsubnet", CF_DATA_TYPE_CONTEXT, IPRANGE_ARGS, &FnCallIsIpInSubnet, "True if an IP address specified in arg2, arg3, ... lies in the range of IP addresses specified in arg1", FNCALL_OPTION_VARARG, FNCALL_CATEGORY_COMM, SYNTAX_STATUS_NORMAL, ARGC(1, -1)), + FnCallTypeNew("isvalidip", CF_DATA_TYPE_CONTEXT, ISVALIDIP_ARGS, &FnCallIsValidIP, "True if arg1 is a valid IPv4 or IPv6 address (only the IP version arg2, if given)", + FNCALL_OPTION_VARARG, FNCALL_CATEGORY_COMM, SYNTAX_STATUS_NORMAL, ARGC(1, 2)), FnCallTypeNew("irange", CF_DATA_TYPE_INT_RANGE, IRANGE_ARGS, &FnCallIRange, "Define a range of integer values for cfengine internal use", FNCALL_OPTION_NONE, FNCALL_CATEGORY_DATA, SYNTAX_STATUS_NORMAL, DEFAULT_ARGC), FnCallTypeNew("isconnectable", CF_DATA_TYPE_CONTEXT, ISCONNECTABLE_ARGS, &FnCallIsConnectable, "Check if a port is connectable", diff --git a/tests/acceptance/02_classes/02_functions/isvalidip.cf b/tests/acceptance/02_classes/02_functions/isvalidip.cf new file mode 100644 index 00000000000..e2b92e4a469 --- /dev/null +++ b/tests/acceptance/02_classes/02_functions/isvalidip.cf @@ -0,0 +1,109 @@ +####################################################### +# +# Test isvalidip() +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent test +{ + meta: + "description" -> { "CFE-3478" } + string => "Test that isvalidip() validates IPv4 and IPv6 addresses"; + + vars: + "valid" + slist => { + "1.2.3.4", + "127.0.0.1", + "5.9.10.22", + "0.0.0.0", + "255.255.255.255", + "FE80:CD00:0000:0CDE:1257:0000:211E:729C", + "fe80:cd00:0:cde:1257:0:211e:729c", + "2001:db8::1", + "::1", + "::", + "::ffff:192.0.2.128", + }; + + "invalid" + slist => { + "", + "10510.17.10", + "100", + "1.2.3", + "1.2.3.4.5", + "233.444.23.1", + "300.123.123.1", + "1.2.3.4/24", + "1.2.3.4:80", + " 1.2.3.4", + "1.2.3.4 ", + "localhost", + "[::1]", + "[::1]:80", + "1::2::3", + "12345::1", + "g::1", + "1:2:3:4:5:6:7:8:9", + "2001:db8::/32", + }; + + "valid_ipv4" slist => { "1.2.3.4", "255.255.255.255" }; + + "invalid_ipv4" + slist => { + "::1", + "::ffff:192.0.2.128", + "FE80:CD00:0000:0CDE:1257:0000:211E:729C", + "300.123.123.1", + }; + + "valid_ipv6" + slist => { + "::1", "::ffff:192.0.2.128", "FE80:CD00:0000:0CDE:1257:0000:211E:729C", + }; + + "invalid_ipv6" slist => { "1.2.3.4", "255.255.255.255", "12345::1" }; +} + +####################################################### +bundle agent check +{ + classes: + "fail" not => isvalidip("$(test.valid)"); + "fail" expression => isvalidip("$(test.invalid)"); + "fail" not => isvalidip("$(test.valid_ipv4)", "ipv4"); + "fail" expression => isvalidip("$(test.invalid_ipv4)", "ipv4"); + "fail" not => isvalidip("$(test.valid_ipv6)", "ipv6"); + "fail" expression => isvalidip("$(test.invalid_ipv6)", "ipv6"); + + reports: + "Wrongly rejected '$(test.valid)'" unless => isvalidip("$(test.valid)"); + "Wrongly accepted '$(test.invalid)'" if => isvalidip("$(test.invalid)"); + + "Wrongly rejected '$(test.valid_ipv4)' as IPv4" + unless => isvalidip("$(test.valid_ipv4)", "ipv4"); + + "Wrongly accepted '$(test.invalid_ipv4)' as IPv4" + if => isvalidip("$(test.invalid_ipv4)", "ipv4"); + + "Wrongly rejected '$(test.valid_ipv6)' as IPv6" + unless => isvalidip("$(test.valid_ipv6)", "ipv6"); + + "Wrongly accepted '$(test.invalid_ipv6)' as IPv6" + if => isvalidip("$(test.invalid_ipv6)", "ipv6"); + + !fail:: + "$(this.promise_filename) Pass"; + + fail:: + "$(this.promise_filename) FAIL"; +} diff --git a/tests/acceptance/02_classes/02_functions/isvalidip_bad_version.x.cf b/tests/acceptance/02_classes/02_functions/isvalidip_bad_version.x.cf new file mode 100644 index 00000000000..e9f08d09e3a --- /dev/null +++ b/tests/acceptance/02_classes/02_functions/isvalidip_bad_version.x.cf @@ -0,0 +1,29 @@ +####################################################### +# +# Test that isvalidip() rejects an unknown IP version +# +####################################################### +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent test +{ + meta: + "description" -> { "CFE-3478" } + string => "Test that isvalidip() fails on an IP version other than ipv4 or ipv6"; + + classes: + "valid" expression => isvalidip("1.2.3.4", "ipv5"); +} + +####################################################### +bundle agent check +{ + reports: + "$(this.promise_filename) Pass"; +}