From c314dea4c1409a3a68fb33706b49e272809b5079 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 20 Sep 2026 19:29:00 +0200 Subject: [PATCH] fix(service-patterns): match library ids on identifier boundaries Distilled from #1245 by Andrew Hundt (d15071c9, the match_qn half). match_qn was a raw strstr(qn, library_id). Short ids fired inside unrelated words, and a service kind REPLACES the plain CALLS edge, so a false match both invents a service edge and deletes a real call: "gin." in plugin. -> route registration; any "/"-prefixed first argument then minted a Route node "nconf" in encryptionconfig -> CONFIGURES for every call into the package "tonic" in Monotonic -> gRPC; unparseable, so NO edge at all "express" in expression -> route registration "get_env" in get_envelope, get_environ -> CONFIGURES "dio" "surf" "curl" "resty" "phin" "hyper" "treq" "rocket" in studio, surface, curly, restyle, dolphin, hyperbolic, streq, rocketmq The rule (qn_hit_on_boundary; every occurrence of an id is tried, not only the first). A separator is any character that is not an ASCII letter or digit. before: start of string | separator | the id itself starts with a separator ("@trpc/server") | the id starts with an uppercase letter -- a capital opens a new CamelCase word whatever precedes it (AsyncHttpClient, IHttpClientFactory, NSURLSession) after: end of string | separator | the id itself ends in a separator ("gin.") | uppercase letter (GuzzleHttp, FeignClient, KafkaProducer) | digit (urllib2, Mint.HTTP2, amqp091-go) Rejected: a lowercase- or digit-initial id glued to a preceding letter or digit, and any id continued by a lowercase letter. Two deliberate differences from the upstream hunk: upstream's before-rule needed a GuzzleHttp table entry and would have dropped IHttpClientFactory and NSURLSession, and without the digit after-boundary urllib2 and amqp091-go stop matching. All three match on main today. No recall is traded for this. No boundary rule can tell "grequests" from "myrequests", so a library whose own name glues a prefix or suffix onto another id gets an explicit entry with the kind (and broker) it had on main -- 47 of them, found mechanically by running main's matcher and the new one over 173 candidate names and listing every one that went from a kind to NONE: HTTP grequests txrequests redaxios gaxios libcurl curlpp curlcpp hyperlocal guzzlehttp ASYNC aiokafka pykafka rskafka librdkafka rdkafkacpp aioamqp amqpstorm pamqp pyamqp amqprs amqpcpp pynats jnats aiomqtt amqtt hbmqtt umqtt mqttools emqtt rumqtt gomqtt libmosquitto mosquittopp pubsublite gocelery CONFIG wgetenv getenvb qgetenv dotenvx phpdotenv ROUTE_REG apiflask honox fasthttprouter GRAPHQL gqlparser gqlgenc aiogqlc Left at NONE on purpose: names that are not clients of the id they contain and were misclassified on main -- hypercorn, hyperlink, hyperopt, openresty, gqlalchemy, celeryconfig, kafkacat, gnatsd, pypubsub. One visible reclassification: hyperium/tonic was HTTP through "hyper" inside "hyperium" and is now gRPC through "tonic". RED, production reverted and the final tests kept: infrascan_service_pattern_match_rejects_ids_inside_words FAIL tests/test_infrascan.c:105: svc_case_mismatches(cases) == 28, expected 0 == 0 4 passed, 1 failed All 28 negatives mismatch on main. The 47 glued-library positives are RED the other way round: against the boundary rule without the entries. Each positive uses a name containing no second id, so it binds to its own entry -- "aiokafka.AIOKafkaProducer" would have passed through "KafkaProducer" and proven nothing. GREEN: infrascan 5, pipeline 281, parallel 74, extraction 350, edge_types_probe 59, route_canon 11, lang_contract 41, cross_repo 8, registry 64, mcp 318 (4 Windows-only skips). 0 failed. Effect on real code -- production binaries, main 92abefa3 against this change. main against main differs by 0 edges and 0 nodes on all three corpora, so every delta below is the change: django CONFIGURES 76 -> 74; both calls return as CALLS (geom.get_envelope, WSGIRequestHandler.get_environ) typescript CONFIGURES 12,026 -> 12,024; one bogus Route "/*type*/" gone with its CALLS and HANDLES; 3 calls return as CALLS kubernetes CONFIGURES 7,962 -> 7,609: all 353 are "nconf" inside encryptionconfig / authorizationconfig / authenticationconfig, and all 353 return as CALLS. 8 bogus Routes gone -- os.ReadFile of the serviceaccount namespace file, t.Run("/TwoWay"), "/tmp/test" -- with 26 CALLS, 2 HANDLES and 21 TESTS edges that pointed at them. 374 plain CALLS restored; 8 of them (validate.Monotonic and friends) had NO edge at all on main. GRPC_CALLS 60 -> 60. HTTP_CALLS 398 -> 407: nine calls main swallowed as route registrations now reach the existing arg_url heuristic like every other package. 88 of the 408 removed service-derived edges were read at the call site -- all of django and typescript, 82 of 402 in kubernetes: 88 false positives, 0 true positives lost. None of the three corpora uses a glued-name library, so the 47 entries are covered by the unit test only. Known limits: "_" is a separator, so optical_fiber.len still matches "fiber." exactly as requests_get matches "requests" by design; a lowercase id followed by a capital matches (kafkaProducer), which also admits a dioXide-style name; a user wrapper glued without a separator (mygetenv) is no longer classified; a glued-name library missing from the list is NONE until someone adds it. Co-authored-by: Andrew Hundt Signed-off-by: Martin Vogel --- internal/cbm/service_patterns.c | 124 ++++++++++++++++++++-- tests/test_infrascan.c | 183 +++++++++++++++++++++++++++++++- 2 files changed, 295 insertions(+), 12 deletions(-) diff --git a/internal/cbm/service_patterns.c b/internal/cbm/service_patterns.c index 0dd4048771..c2425e09e1 100644 --- a/internal/cbm/service_patterns.c +++ b/internal/cbm/service_patterns.c @@ -140,6 +140,20 @@ static const lib_pattern_t http_libraries[] = { {"socket.http", CBM_SVC_HTTP, NULL}, {"resty.http", CBM_SVC_HTTP, NULL}, + /* Glued-name libraries. match_qn needs an identifier boundary around an + * id, so a library whose own name glues a lowercase prefix/suffix onto + * another id ("grequests", "curlpp") is listed explicitly: no boundary + * rule can tell "grequests" from "myrequests". */ + {"grequests", CBM_SVC_HTTP, NULL}, /* Python: gevent + requests */ + {"txrequests", CBM_SVC_HTTP, NULL}, /* Python: Twisted + requests */ + {"redaxios", CBM_SVC_HTTP, NULL}, /* JS: axios-compatible fetch client */ + {"gaxios", CBM_SVC_HTTP, NULL}, /* JS: Google's axios-style client */ + {"libcurl", CBM_SVC_HTTP, NULL}, /* C, node-libcurl */ + {"curlpp", CBM_SVC_HTTP, NULL}, /* C++ libcurl wrapper */ + {"curlcpp", CBM_SVC_HTTP, NULL}, /* C++ libcurl wrapper */ + {"hyperlocal", CBM_SVC_HTTP, NULL}, /* Rust: hyper over unix sockets */ + {"guzzlehttp", CBM_SVC_HTTP, NULL}, /* PHP: Guzzle's composer vendor */ + {NULL, CBM_SVC_NONE, NULL}, }; @@ -263,6 +277,33 @@ static const lib_pattern_t async_libraries[] = { {"dapr.clients.grpc", CBM_SVC_ASYNC, "dapr"}, {"DaprClient", CBM_SVC_ASYNC, "dapr"}, + /* Glued-name libraries (see the note in http_libraries). */ + {"aiokafka", CBM_SVC_ASYNC, "kafka"}, + {"pykafka", CBM_SVC_ASYNC, "kafka"}, + {"librdkafka", CBM_SVC_ASYNC, "kafka"}, + {"rdkafkacpp", CBM_SVC_ASYNC, "kafka"}, + {"rskafka", CBM_SVC_ASYNC, "kafka"}, + {"aioamqp", CBM_SVC_ASYNC, "rabbitmq"}, + {"amqpstorm", CBM_SVC_ASYNC, "rabbitmq"}, + {"pamqp", CBM_SVC_ASYNC, "rabbitmq"}, + {"pyamqp", CBM_SVC_ASYNC, "rabbitmq"}, + {"amqprs", CBM_SVC_ASYNC, "rabbitmq"}, + {"amqpcpp", CBM_SVC_ASYNC, "rabbitmq"}, + {"pynats", CBM_SVC_ASYNC, "nats"}, + {"jnats", CBM_SVC_ASYNC, "nats"}, + {"aiomqtt", CBM_SVC_ASYNC, "mqtt"}, + {"amqtt", CBM_SVC_ASYNC, "mqtt"}, + {"hbmqtt", CBM_SVC_ASYNC, "mqtt"}, + {"umqtt", CBM_SVC_ASYNC, "mqtt"}, + {"mqttools", CBM_SVC_ASYNC, "mqtt"}, + {"emqtt", CBM_SVC_ASYNC, "mqtt"}, + {"rumqtt", CBM_SVC_ASYNC, "mqtt"}, + {"gomqtt", CBM_SVC_ASYNC, "mqtt"}, + {"libmosquitto", CBM_SVC_ASYNC, "mqtt"}, + {"mosquittopp", CBM_SVC_ASYNC, "mqtt"}, + {"pubsublite", CBM_SVC_ASYNC, "pubsub"}, + {"gocelery", CBM_SVC_ASYNC, "celery"}, + {NULL, CBM_SVC_NONE, NULL}, }; @@ -308,6 +349,13 @@ static const lib_pattern_t config_libraries[] = { {"Application.get_env", CBM_SVC_CONFIG, NULL}, {"Application.fetch_env", CBM_SVC_CONFIG, NULL}, + /* Glued-name accessors (see the note in http_libraries). */ + {"wgetenv", CBM_SVC_CONFIG, NULL}, /* Windows CRT: _wgetenv, _wgetenv_s */ + {"getenvb", CBM_SVC_CONFIG, NULL}, /* Python: os.getenvb */ + {"qgetenv", CBM_SVC_CONFIG, NULL}, /* Qt */ + {"dotenvx", CBM_SVC_CONFIG, NULL}, /* JS: @dotenvx/dotenvx */ + {"phpdotenv", CBM_SVC_CONFIG, NULL}, /* PHP: vlucas/phpdotenv */ + {NULL, CBM_SVC_NONE, NULL}, }; @@ -377,6 +425,11 @@ static const lib_pattern_t route_reg_libraries[] = { {"akka.http.scaladsl.server", CBM_SVC_ROUTE_REG, NULL}, {"play.api.routing", CBM_SVC_ROUTE_REG, NULL}, + /* Glued-name frameworks (see the note in http_libraries). */ + {"apiflask", CBM_SVC_ROUTE_REG, NULL}, /* Python: Flask-based */ + {"honox", CBM_SVC_ROUTE_REG, NULL}, /* JS: Hono meta-framework */ + {"fasthttprouter", CBM_SVC_ROUTE_REG, NULL}, /* Go: httprouter for fasthttp */ + {NULL, CBM_SVC_NONE, NULL}, }; @@ -448,6 +501,11 @@ static const lib_pattern_t graphql_libraries[] = { {"async-graphql", CBM_SVC_GRAPHQL, NULL}, {"juniper", CBM_SVC_GRAPHQL, NULL}, + /* Glued-name libraries (see the note in http_libraries). */ + {"gqlparser", CBM_SVC_GRAPHQL, NULL}, /* Go: vektah/gqlparser */ + {"gqlgenc", CBM_SVC_GRAPHQL, NULL}, /* Go: gqlgen client generator */ + {"aiogqlc", CBM_SVC_GRAPHQL, NULL}, /* Python: asyncio GraphQL client */ + {NULL, CBM_SVC_NONE, NULL}, }; @@ -534,17 +592,67 @@ static const method_suffix_t method_suffixes[] = { /* ── Matching implementation ───────────────────────────────────── */ -/* Check if any library identifier appears as a substring in the QN. - * Case-sensitive: "requests" matches "project.venv.requests.api.get" - * but not "Requests". Library names are specific enough to avoid - * false positives even with substring matching. */ +static bool qn_is_lower(char ch) { + return ch >= 'a' && ch <= 'z'; +} + +static bool is_digit_char(char ch) { + return ch >= '0' && ch <= '9'; +} + +static bool qn_is_alnum(char ch) { + return qn_is_lower(ch) || is_digit_char(ch) || (ch >= 'A' && ch <= 'Z'); +} + +/* True when the occurrence of `id` at `hit` sits on identifier boundaries. + * + * A raw substring match let short ids fire inside unrelated words: "gin." + * in "plugin.", "dio" in "studio", "surf" in "surface", "express" in + * "expression". Every character that is not an ASCII letter or digit is a + * separator ('.', '/', '\\', ':', '_', '-', '$', '@', ...). + * + * BEFORE: start of string, a separator, an id that itself starts with a + * separator ("@trpc/server"), or an id that starts with an + * uppercase letter — a capital opens a new CamelCase word whatever + * precedes it ("AsyncHttpClient", "IHttpClientFactory", + * "NSURLSession"). Rejected: a lowercase/digit-initial id glued to + * a letter or digit ("plugin." / "studio" / "myrequests"). + * AFTER: end of string, a separator, an id that itself ends in a + * separator ("gin."), an uppercase letter (CamelCase glue: + * "GuzzleHttp", "FeignClient", "KafkaProducer", "kafkaProducer") + * or a digit (version suffix: "urllib2", "amqp091-go"). + * Rejected: a lowercase letter continuing the word ("surface", + * "curly", "expression"). */ +static bool qn_hit_on_boundary(const char *qn, const char *hit, const char *id, size_t id_len) { + char first = id[0]; + char last = id[id_len - 1]; + if (hit > qn && qn_is_alnum(hit[-1]) && (qn_is_lower(first) || is_digit_char(first))) { + return false; + } + if (qn_is_alnum(last) && qn_is_lower(hit[id_len])) { + return false; + } + return true; +} + +/* Check if any library identifier appears in the QN on identifier + * boundaries (see qn_hit_on_boundary). Case-sensitive: "requests" matches + * "project.venv.requests.api.get" and "svc.requests_get" but neither + * "Requests" nor "myrequests". */ static const lib_pattern_t *match_qn(const char *qn, const lib_pattern_t *patterns) { if (!qn || !qn[0]) { return NULL; } for (int i = 0; patterns[i].library_id != NULL; i++) { - if (strstr(qn, patterns[i].library_id) != NULL) { - return &patterns[i]; + const char *id = patterns[i].library_id; + size_t id_len = strlen(id); + if (id_len == 0) { + continue; + } + for (const char *hit = strstr(qn, id); hit != NULL; hit = strstr(hit + 1, id)) { + if (qn_hit_on_boundary(qn, hit, id, id_len)) { + return &patterns[i]; + } } } return NULL; @@ -574,10 +682,6 @@ static bool contains_segment(const char *path, const char *segment) { return false; } -static bool is_digit_char(char ch) { - return ch >= '0' && ch <= '9'; -} - static bool has_http_route_marker(const char *path) { if (starts_with_segment(path, "api") || starts_with_segment(path, "apis") || starts_with_segment(path, "graphql") || starts_with_segment(path, "health") || diff --git a/tests/test_infrascan.c b/tests/test_infrascan.c index cd7df945d0..a9bf1aef94 100644 --- a/tests/test_infrascan.c +++ b/tests/test_infrascan.c @@ -1,11 +1,11 @@ #include "test_framework.h" #include "graph_buffer/graph_buffer.h" #include "pipeline/pipeline_internal.h" +#include "service_patterns.h" #include #include - -bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name); +#include static int has_data_flow(cbm_gbuf_t *gb, int64_t source_id, int64_t target_id) { const cbm_gbuf_edge_t **edges = NULL; @@ -48,6 +48,183 @@ TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths) { PASS(); } +/* ── Service-pattern QN boundaries (distilled from PR #1245) ───────── + * A library id must sit on an identifier boundary inside the QN. A raw + * substring match classified "proj.plugin.loader" as gin ("gin.") and + * "proj.studio.x" as Dio ("dio"), which minted bogus Route / HTTP_CALLS + * edges. The rule must stay open for CamelCase glue ("GuzzleHttp", + * "FeignClient", "KafkaProducer", "IHttpClientFactory") and for version + * suffixes ("urllib2", "amqp091-go"). */ +typedef struct { + const char *qn; + cbm_svc_kind_t want; +} svc_case_t; + +static int svc_case_mismatches(const svc_case_t *cases) { + int bad = 0; + for (int i = 0; cases[i].qn != NULL; i++) { + cbm_svc_kind_t got = cbm_service_pattern_match(cases[i].qn); + if (got != cases[i].want) { + printf(" service-pattern mismatch: \"%s\" -> %d, expected %d\n", cases[i].qn, (int)got, + (int)cases[i].want); + bad++; + } + } + return bad; +} + +TEST(infrascan_service_pattern_match_rejects_ids_inside_words) { + static const svc_case_t cases[] = { + /* "gin." inside plugin. */ + {"proj.plugins.autorun.tests.test_plugin._dispatch", CBM_SVC_NONE}, + {"proj.plugin.loader", CBM_SVC_NONE}, + /* "chi." / "echo." / "fiber." */ + {"proj.hibachi.grill", CBM_SVC_NONE}, + {"sound.reecho.apply", CBM_SVC_NONE}, + {"textile.microfiber.weave", CBM_SVC_NONE}, + /* "dio" inside studio / radio */ + {"proj.studio.x", CBM_SVC_NONE}, + {"radio.tune", CBM_SVC_NONE}, + /* "surf" / "resty" / "curl" / "phin" / "hyper" / "sling" / "needle" / "treq" */ + {"app.surface.draw", CBM_SVC_NONE}, + {"ui.restyle.apply", CBM_SVC_NONE}, + /* control: "forestry" spells "restr", never contained "resty" */ + {"pkg.forestry.trees", CBM_SVC_NONE}, + {"util.curly.brace", CBM_SVC_NONE}, + {"sea.dolphin.swim", CBM_SVC_NONE}, + {"math.hyperbolic.tanh", CBM_SVC_NONE}, + {"farm.gosling.walk", CBM_SVC_NONE}, + {"sewing.needles.sort", CBM_SVC_NONE}, + {"util.streq", CBM_SVC_NONE}, + /* "requests" glued to a lowercase prefix (upstream's case) */ + {"proj.myrequests.client.get", CBM_SVC_NONE}, + /* "express" / "rocket" / "tonic" / "nconf" */ + {"ast.expression.eval", CBM_SVC_NONE}, + {"mq.rocketmq.send", CBM_SVC_NONE}, + {"geo.tectonic.plates", CBM_SVC_NONE}, + {"app.unconfigured.warn", CBM_SVC_NONE}, + {"cli.runconfig.load", CBM_SVC_NONE}, + {"aws.s3express.session", CBM_SVC_NONE}, + /* not clients of the id they contain: ASGI server, URL library, + * nginx platform, Memgraph OGM, Celery settings module, a word */ + {"hypercorn.run.serve", CBM_SVC_NONE}, + {"hyperlink.URL.from_text", CBM_SVC_NONE}, + {"openresty.core.request", CBM_SVC_NONE}, + {"gqlalchemy.Memgraph.execute", CBM_SVC_NONE}, + {"celeryconfig.broker_url", CBM_SVC_NONE}, + {"kafkaesque.story.tell", CBM_SVC_NONE}, + {NULL, CBM_SVC_NONE}, + }; + ASSERT_EQ(svc_case_mismatches(cases), 0); + PASS(); +} + +TEST(infrascan_service_pattern_match_keeps_real_library_qns) { + static const svc_case_t cases[] = { + /* Route registration: ids that end in a separator, own segment */ + {"github.com/gin-gonic/gin.Default", CBM_SVC_ROUTE_REG}, + {"proj.gin.router.GET", CBM_SVC_ROUTE_REG}, + {"gin.GET", CBM_SVC_ROUTE_REG}, + {"chi.NewRouter", CBM_SVC_ROUTE_REG}, + {"github.com/labstack/echo.New", CBM_SVC_ROUTE_REG}, + {"fiber.New", CBM_SVC_ROUTE_REG}, + {"proj.express.router.get", CBM_SVC_ROUTE_REG}, + {"@hapi/hapi.server.route", CBM_SVC_ROUTE_REG}, + /* HTTP: own segment / separator-joined wrapper */ + {"proj.venv.requests.api.get", CBM_SVC_HTTP}, + {"proj.service.requests_get", CBM_SVC_HTTP}, + {"package:dio/dio.dart", CBM_SVC_HTTP}, + {"dio.Dio.get", CBM_SVC_HTTP}, + {"pkg.net.curl.get", CBM_SVC_HTTP}, + {"curl_exec", CBM_SVC_HTTP}, + {"surf.get", CBM_SVC_HTTP}, + {"hyper.Client.request", CBM_SVC_HTTP}, + {"phin.promisified", CBM_SVC_HTTP}, + {"needle.get", CBM_SVC_HTTP}, + {"github.com/go-resty/resty.New", CBM_SVC_HTTP}, + {"github.com/dghubble/sling.New", CBM_SVC_HTTP}, + /* HTTP: CamelCase glue after the id */ + {"GuzzleHttp\\Client", CBM_SVC_HTTP}, + {"proj.GuzzleHttp.Client.get", CBM_SVC_HTTP}, + {"org.springframework.cloud.openfeign.FeignClientBuilder.build", CBM_SVC_HTTP}, + {"com.acme.net.HttpClientFactory.create", CBM_SVC_HTTP}, + /* HTTP: CamelCase glue before the id */ + {"System.Net.Http.IHttpClientFactory.CreateClient", CBM_SVC_HTTP}, + {"org.asynchttpclient.AsyncHttpClient.prepareGet", CBM_SVC_HTTP}, + {"Foundation.NSURLSession.dataTask", CBM_SVC_HTTP}, + /* HTTP: version suffix after the id */ + {"urllib2.urlopen", CBM_SVC_HTTP}, + {"Mint.HTTP2.request", CBM_SVC_HTTP}, + /* Async */ + {"com.acme.events.KafkaProducer.send", CBM_SVC_ASYNC}, + {"this.kafkaProducer.send", CBM_SVC_ASYNC}, + {"org.apache.kafka.clients.producer.KafkaProducer.send", CBM_SVC_ASYNC}, + {"github.com/rabbitmq/amqp091-go.Channel.Publish", CBM_SVC_ASYNC}, + {"github.com/nats-io/nats.go.Conn.Publish", CBM_SVC_ASYNC}, + /* Config */ + {"os.getenv", CBM_SVC_CONFIG}, + {"os.Getenv", CBM_SVC_CONFIG}, + /* Glued-name libraries: the library's own name glues a lowercase + * prefix/suffix onto another id, so each has an explicit table entry. + * Every QN below contains NO other id — only the entry can match. */ + {"grequests.map", CBM_SVC_HTTP}, + {"txrequests.Session.send", CBM_SVC_HTTP}, + {"redaxios.create", CBM_SVC_HTTP}, + {"gaxios.request", CBM_SVC_HTTP}, + {"libcurl.version", CBM_SVC_HTTP}, + {"node-libcurl.Easy.perform", CBM_SVC_HTTP}, + {"curlpp.Easy.perform", CBM_SVC_HTTP}, + {"curlcpp.easy.perform", CBM_SVC_HTTP}, + {"hyperlocal.UnixConnector.call", CBM_SVC_HTTP}, + {"guzzlehttp/psr7.Utils.streamFor", CBM_SVC_HTTP}, + {"aiokafka.helpers.create_ssl_context", CBM_SVC_ASYNC}, + {"pykafka.topic.Topic.get_producer", CBM_SVC_ASYNC}, + {"librdkafka.rd_version", CBM_SVC_ASYNC}, + {"rdkafkacpp.version", CBM_SVC_ASYNC}, + {"rskafka.client.partition.write", CBM_SVC_ASYNC}, + {"aioamqp.connect", CBM_SVC_ASYNC}, + {"amqpstorm.Connection.channel", CBM_SVC_ASYNC}, + {"pamqp.frame.marshal", CBM_SVC_ASYNC}, + {"kombu.transport.pyamqp.Transport.establish_connection", CBM_SVC_ASYNC}, + {"amqprs.channel.basic_publish", CBM_SVC_ASYNC}, + {"amqpcpp.TcpChannel.publish", CBM_SVC_ASYNC}, + {"pynats.client.send", CBM_SVC_ASYNC}, + {"jnats.options.build", CBM_SVC_ASYNC}, + {"aiomqtt.client.publish", CBM_SVC_ASYNC}, + {"amqtt.session.deliver", CBM_SVC_ASYNC}, + {"hbmqtt.session.deliver", CBM_SVC_ASYNC}, + {"umqtt.simple.publish", CBM_SVC_ASYNC}, + {"mqttools.client.publish", CBM_SVC_ASYNC}, + {"emqtt.publish", CBM_SVC_ASYNC}, + {"rumqtt.client.publish", CBM_SVC_ASYNC}, + {"github.com/256dpi/gomqtt/client.Publish", CBM_SVC_ASYNC}, + {"libmosquitto.publish", CBM_SVC_ASYNC}, + {"mosquittopp.publish", CBM_SVC_ASYNC}, + {"google.cloud.pubsublite.admin.create_topic", CBM_SVC_ASYNC}, + {"gocelery.client.delay", CBM_SVC_ASYNC}, + {"_wgetenv", CBM_SVC_CONFIG}, + {"_wgetenv_s", CBM_SVC_CONFIG}, + {"os.getenvb", CBM_SVC_CONFIG}, + {"qgetenv", CBM_SVC_CONFIG}, + {"@dotenvx/dotenvx.config", CBM_SVC_CONFIG}, + {"vlucas/phpdotenv.load", CBM_SVC_CONFIG}, + {"apiflask.scaffold.get", CBM_SVC_ROUTE_REG}, + {"honox.factory.createRoute", CBM_SVC_ROUTE_REG}, + {"fasthttprouter.Router.GET", CBM_SVC_ROUTE_REG}, + {"github.com/vektah/gqlparser/v2.LoadSchema", CBM_SVC_GRAPHQL}, + {"github.com/Yamashou/gqlgenc.client.Post", CBM_SVC_GRAPHQL}, + {"aiogqlc.client.execute", CBM_SVC_GRAPHQL}, + {NULL, CBM_SVC_NONE}, + }; + ASSERT_EQ(svc_case_mismatches(cases), 0); + ASSERT_STR_EQ(cbm_service_pattern_broker("aiokafka.helpers.create_ssl_context"), "kafka"); + ASSERT_STR_EQ(cbm_service_pattern_broker("umqtt.simple.publish"), "mqtt"); + ASSERT_STR_EQ(cbm_service_pattern_broker("com.acme.events.KafkaProducer.send"), "kafka"); + ASSERT_STR_EQ(cbm_service_pattern_broker("github.com/rabbitmq/amqp091-go.Channel.Publish"), + "rabbitmq"); + PASS(); +} + TEST(infrascan_route_nodes_skip_bad_http_url_paths) { cbm_gbuf_t *gb = cbm_gbuf_new("test", "/tmp/cbm_infrascan_route_guard"); ASSERT_NOT_NULL(gb); @@ -128,6 +305,8 @@ TEST(infrascan_http_calls_join_matching_handler_route) { SUITE(infrascan) { RUN_TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths); + RUN_TEST(infrascan_service_pattern_match_rejects_ids_inside_words); + RUN_TEST(infrascan_service_pattern_match_keeps_real_library_qns); RUN_TEST(infrascan_route_nodes_skip_bad_http_url_paths); RUN_TEST(infrascan_http_calls_join_matching_handler_route); }