Title
#extensions
Daniel Bretón Suárez

Daniel Bretón Suárez

11/08/2022, 1:27 PM
Hi! I'm trying to build a
config_parser
in a C++ extension. I'm using
REGISTER_EXTERNAL
macro and I see the debug message at
registry_factory.cpp
I1108 12:56:08.491199 231789 registry_factory.cpp:107] Extension 499 registered config_parser plugin devo_params
However, the update function is never called. If I add some debug messages to
osquery/config/config.cpp
and print the list of all config_parser modules it handles, I can't see it. However, if I add the same function at the extension code, it exists! Any ideas why this could be happening?
void printAll()
{
  auto plugins = osquery::RegistryFactory::get().plugins("config_parser");
  for (auto & p : plugins) {
    printf("registered parser %s\n", p.first.c_str());
  }
}
Stefano Bonicatti

Stefano Bonicatti

11/08/2022, 6:24 PM
How are you starting the extension and osquery? What flags?
Daniel Bretón Suárez

Daniel Bretón Suárez

11/09/2022, 11:06 AM
I'm running osquery daemon in verbose mode
clear; sudo osqueryd --flagfile "/etc/osquery/osquery.flags" --verbose 2>&1 | tee devo-ea-agent-verbose.out
With this flagfile
--enroll_secret_path=/etc/osquery/certs/secret
--tls_server_certs=/etc/osquery/certs/devo-ea-manager.crt
--tls_hostname=devo-ea-manager:8080
--enroll_tls_endpoint=/api/v1/osquery/enroll
--config_plugin=tls
--config_tls_endpoint=/api/v1/osquery/config
--config_refresh=10
--disable_distributed=false
--distributed_plugin=tls
--distributed_interval=3
--distributed_tls_max_attempts=3
--distributed_tls_read_endpoint=/api/v1/osquery/distributed/read
--distributed_tls_write_endpoint=/api/v1/osquery/distributed/write
--logger_plugin=tls
--logger_tls_endpoint=/api/v1/osquery/log
--logger_tls_period=10
--logger_tls_max_lines=8192
--extensions_autoload=/etc/osquery/extensions.load
--watchdog_memory_limit=512
--enable_extensions_watchdog=true
--extensions_require=system-stats,fetchfiles,devo-wevent-logger
--disable_tables=curl
An this extension.load (devo_wevent_logger is the one I'm working on)
/opt/osquery/share/osquery/extensions/fetchfiles.ext
/opt/osquery/share/osquery/extensions/devo_wevent_logger.ext
/opt/osquery/share/osquery/extensions/system-stats.ext
I'm also running a fleet server in a VM. The ultimate goal is to distribute the configuration. Just to be clear, I can access the parameters defined as
custom_
flags under
options
as in the first image. But I want to implement a schema as the second image
Stefano Bonicatti

Stefano Bonicatti

11/09/2022, 11:30 AM
Nvm I misread the image! So it is indeed a separate section already (got confused by the first image)
11:33 AM
I see though that in your previous example the plugin is named “devo_params”, while the section you’re trying to parse is called “devo_things”
11:35 AM
Afaik they have to match; for the reason of why printAll doesn’t print your config parser, not sure, where exactly have you placed that printAll call?
Daniel Bretón Suárez

Daniel Bretón Suárez

11/09/2022, 11:38 AM
The invocation of printAll that does not print the parser is placed at
Config::refresh()
under
osquery/config/config.cpp
The one that prints the parser is under the extension
12:01 PM
I'm changing it for the names to match, but I'm not able to get the update function from my config_parser called when I change the configuration on the manager. I think that if it is not listed under the registered parsers on the core process it won't be called. However, it is listed in the extension process. I was thinking that could be the root problem, but now I'm not sure. Am I missing something?
Stefano Bonicatti

Stefano Bonicatti

11/09/2022, 12:12 PM
Ok, sorry my extension game is rusty. As far as I can see we don’t support external config parsers, while a config plugin we do. The plugin gets registered in a separate registry, the external one, but the parsers use only the local one.
12:20 PM
More specifically, at a certain point in core you get the
RegisteryFactory::addBroadcast
function called: https://github.com/osquery/osquery/blob/master/osquery/registry/registry_factory.cpp#L68 Then on line 100 we register the plugin provided by the extension using the specific registry
addExternal
function; that ends up calling the RegistryInterface relative function https://github.com/osquery/osquery/blob/master/osquery/registry/registry_interface.cpp#L256 which at line 270 registers the route to that plugin in external_; before that it calls the
addExternalPlugin
, which you can follow in our plugin.h which is basically empty and always returns success, the only thing that implements it is a TablePlugin, which uses SQL to make the table appear in the local sqlite database. Now a Config plugin works because the code uses the
call
function of the Registry, which automatically uses all the registry types; you can see its implementation here:https://github.com/osquery/osquery/blob/master/osquery/registry/registry_interface.cpp#L133 As you can see that also uses the routes previously registered in the external registry
12:27 PM
Sorry forgot to point where the Config plugin does the call: https://github.com/osquery/osquery/blob/0b4ec101e86a35d5855a45bb03324abbfe5688a6/osquery/config/config.cpp#L484 And instead you can see that the applyParsers function uses the function you were trying to use to get the registered plugins: https://github.com/osquery/osquery/blob/0b4ec101e86a35d5855a45bb03324abbfe5688a6/osquery/config/config.cpp#L819 But that
plugins("config_parser")
function only gets local plugins; after having select the
config_parsers
registry, it ends up in the
plugins()
function of the
RegistryInterface
, here: https://github.com/osquery/osquery/blob/master/osquery/registry/registry_interface.cpp#L330 As you can see it’s returning the
items_
member, but in RegistryInterface you also have
external_
, which is where the external plugins are.
12:29 PM
TLDR: You need to use a ConfigPlugin which has to be provided via
--config_plugin
, which will receive the whole config and will have to select and modify only what’s interested in.
1:15 PM
So the TLDR might need to be expanded: the situation is a bit messy; you can request from the extension that each osquery local plugin should generate again the config and results to be sent to the extension, which if nothing changed it would be the same as what osquery sees. You would do that through the registry, using a call similar to what the core does in reverse here: https://github.com/osquery/osquery/blob/0b4ec101e86a35d5855a45bb03324abbfe5688a6/osquery/config/config.cpp#L484 That though has its problems, because for some plugins that might means an additional remote call (for the TLS plugin); you’re also getting the individual plugins configs, each potentially at different points in time which might not match with what osquery has applied. I think that mostly works if the config you need to read is the config that exists in the config file on the host, and so you request it to the filesystem plugin. Though by its nature that config cannot be changed through Fleet (or “you can”, but that actually is a layer on top that comes from the TLS plugin; that layer won’t be seen from the extension). The ConfigPlugin way should be technically there to add to the config view osquery has, not just to read; moreover to get the full view, you need to put your own config plugin name as the last in the list in
--config_plugin
. In theory each plugin before should compose a final response which contains everything the core has to use; so the last plugin in the list should get all.
Daniel Bretón Suárez

Daniel Bretón Suárez

11/09/2022, 2:57 PM
Wow, awesome explanation. I need some time to process all this info but is really really useful. Thank you a lot for your patience
11:54 AM
FYI, I was finally able to get the whole distributed config using
osquery::Registry::call("config", {{"action", "genConfig"}}, config);
and then, parsing it into a map the same way it is done at https://github.com/osquery/osquery/blob/f8bd96e1ad499397596e3fb551ac8f0065e43c32/plugins/config/parsers/options.cpp#L76
std::map<std::string, std::string> 
    Config::parseDistConfig(const osquery::PluginResponse & config, 
                            std::string plugin, std::string cat)
{
  std::map<std::string, std::string> ret;
  for (auto conf : config) {
    for (auto c1 : conf) {
      osquery::JSON json;
      json.fromString(c1.second);
      const auto& options = json.doc()[plugin][cat];
      for (const auto& option : options.GetObject()) {
        std::string name = option.name.GetString();
        std::string value;
        if (option.value.IsString()) {
          value = option.value.GetString();
        } else if (option.value.IsBool()) {
          value = (option.value.GetBool()) ? "true" : "false";
        } else if (option.value.IsInt()) {
          value = std::to_string(option.value.GetInt());
        } else if (option.value.IsNumber()) {
          value = std::to_string(option.value.GetUint64());
        } else if (option.value.IsObject() || option.value.IsArray()) {
          auto doc = osquery::JSON::newFromValue(option.value);
          doc.toString(value);
        } else {
          DEVOLOG(WARNING) << "Cannot parse unknown value type for option: " << name;
        }
        ret.insert({name, value});
      }
    }
  }
  return ret;
}