ADD_CUSTOM_COMMAND(
OUTPUT ${AMALGAMATION_FILE_GEN}
COMMAND
- python "${OSQUERY_CODEGEN_PATH}/amalgamate.py" "${OSQUERY_CODEGEN_PATH}" "${OSQUERY_GENERATED_PATH}"
+ python "${OSQUERY_CODEGEN_PATH}/amalgamate.py"
+ --templates "${OSQUERY_CODEGEN_PATH}/templates"
+ --sources "${OSQUERY_GENERATED_PATH}"
+ --output "${AMALGAMATION_FILE_GEN}"
DEPENDS
${GENERATED_TABLES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
-table_name("groups")
-description("Local system groups.")
-schema([
- Column("gid", BIGINT, "Unsigned int64 group ID"),
- Column("gid_signed", BIGINT, "A signed int64 version of gid"),
- Column("groupname", TEXT, "Canonical local group name"),
-])
-implementation("groups@genGroups")
-examples([
- "select * from groups where gid = 0",
- # Group/user_groups is not JOIN optimized
- #"select g.groupname, ug.uid from groups g, user_groups ug where g.gid = ug.gid",
-])
+table_name("groups")\r
+description("Local system groups.")\r
+schema([\r
+ Column("gid", BIGINT, "Unsigned int64 group ID", index=True),\r
+ Column("gid_signed", BIGINT, "A signed int64 version of gid"),\r
+ Column("groupname", TEXT, "Canonical local group name"),\r
+])\r
+extended_schema(WINDOWS, [\r
+ Column("group_sid", TEXT, "Unique group ID", index=True),\r
+ Column("comment", TEXT, "Remarks or comments associated with the group"),\r
+])\r
+\r
+extended_schema(DARWIN, [\r
+ Column("is_hidden", INTEGER, "IsHidden attribute set in OpenDirectory"),\r
+])\r
+implementation("groups@genGroups")\r
+examples([\r
+ "select * from groups where gid = 0",\r
+ # Group/user_groups is not JOIN optimized\r
+ #"select g.groupname, ug.uid from groups g, user_groups ug where g.gid = ug.gid",\r
+ # The relative group ID, or RID, is used by osquery as the "gid"\r
+ # For Windows, "gid" and "gid_signed" will always be the same.\r
+])\r
table_name("processes")
description("All running processes on the host system.")
schema([
- Column("pid", INTEGER, "Process (or thread) ID", index=True),
+ Column("pid", BIGINT, "Process (or thread) ID", index=True),
Column("name", TEXT, "The process path or shorthand argv[0]"),
Column("path", TEXT, "Path to executed binary"),
Column("cmdline", TEXT, "Complete argv"),
+ Column("state", TEXT, "Process state"),
Column("cwd", TEXT, "Process current working directory"),
Column("root", TEXT, "Process virtual root directory"),
Column("uid", BIGINT, "Unsigned user ID"),
- Column("gid", BIGINT, "Unsgiend groud ID"),
+ Column("gid", BIGINT, "Unsigned group ID"),
Column("euid", BIGINT, "Unsigned effective user ID"),
Column("egid", BIGINT, "Unsigned effective group ID"),
- Column("on_disk", TEXT, "The process path exists yes=1, no=0, unknown=-1"),
- Column("wired_size", TEXT, "Bytes of unpagable memory used by process"),
- Column("resident_size", TEXT, "Bytes of private memory used by process"),
- Column("phys_footprint", TEXT, "Bytes of total physical memory used"),
- Column("user_time", TEXT, "CPU time spent in user space"),
- Column("system_time", TEXT, "CPU time spent in kernel space"),
- Column("start_time", TEXT, "Unix timestamp of process start"),
- Column("parent", INTEGER, "Process parent's PID"),
+ Column("suid", BIGINT, "Unsigned saved user ID"),
+ Column("sgid", BIGINT, "Unsigned saved group ID"),
+ Column("on_disk", INTEGER,
+ "The process path exists yes=1, no=0, unknown=-1"),
+ Column("wired_size", BIGINT, "Bytes of unpagable memory used by process"),
+ Column("resident_size", BIGINT, "Bytes of private memory used by process"),
+ Column("total_size", BIGINT, "Total virtual memory size",
+ aliases=["phys_footprint"]),
+ Column("user_time", BIGINT, "CPU time in milliseconds spent in user space"),
+ Column("system_time", BIGINT, "CPU time in milliseconds spent in kernel space"),
+ Column("disk_bytes_read", BIGINT, "Bytes read from disk"),
+ Column("disk_bytes_written", BIGINT, "Bytes written to disk"),
+ Column("start_time", BIGINT, "Process start time in seconds since Epoch, in case of error -1"),
+ Column("parent", BIGINT, "Process parent's PID"),
+ Column("pgroup", BIGINT, "Process group"),
+ Column("threads", INTEGER, "Number of threads used by process"),
+ Column("nice", INTEGER, "Process nice level (-20 to 20, default 0)"),
])
+extended_schema(WINDOWS, [
+ Column("is_elevated_token", INTEGER, "Process uses elevated token yes=1, no=0"),
+ Column("elapsed_time", BIGINT, "Elapsed time in seconds this process has been running."),
+ Column("handle_count", BIGINT, "Total number of handles that the process has open. This number is the sum of the handles currently opened by each thread in the process."),
+ Column("percent_processor_time", BIGINT, "Returns elapsed time that all of the threads of this process used the processor to execute instructions in 100 nanoseconds ticks."),
+])
+extended_schema(DARWIN, [
+ Column("upid", BIGINT, "A 64bit pid that is never reused. Returns -1 if we couldn't gather them from the system."),
+ Column("uppid", BIGINT, "The 64bit parent pid that is never reused. Returns -1 if we couldn't gather them from the system."),
+ Column("cpu_type", INTEGER, "A 64bit pid that is never reused. Returns -1 if we couldn't gather them from the system."),
+ Column("cpu_subtype", INTEGER, "The 64bit parent pid that is never reused. Returns -1 if we couldn't gather them from the system."),
+])
+attributes(cacheable=True, strongly_typed_rows=True)
implementation("system/processes@genProcesses")
examples([
"select * from processes where pid = 1",
table_name("users")
-description("Local system users.")
+description("Local user accounts (including domain accounts that have logged on locally (Windows)).")
schema([
- Column("uid", BIGINT, "User ID"),
+ Column("uid", BIGINT, "User ID", index=True),
Column("gid", BIGINT, "Group ID (unsigned)"),
Column("uid_signed", BIGINT, "User ID as int64 signed (Apple)"),
Column("gid_signed", BIGINT, "Default group ID as int64 signed (Apple)"),
- Column("username", TEXT, "Username"),
+ Column("username", TEXT, "Username", additional=True),
Column("description", TEXT, "Optional user description"),
Column("directory", TEXT, "User's home directory"),
Column("shell", TEXT, "User's configured default shell"),
+ Column("uuid", TEXT, "User's UUID (Apple) or SID (Windows)"),
+])
+extended_schema(WINDOWS, [
+ Column("type", TEXT, "Whether the account is roaming (domain), local, or a system profile"),
+])
+
+extended_schema(DARWIN, [
+ Column("is_hidden", INTEGER, "IsHidden attribute set in OpenDirectory")
])
implementation("users@genUsers")
-implementation_update("users@updateUsers")
examples([
"select * from users where uid = 1000",
"select * from users where username = 'root'",
--- /dev/null
+table_name("file")
+description("Interactive filesystem attributes and metadata.")
+schema([
+ Column("path", TEXT, "Absolute file path", required=True, index=True),
+ Column("directory", TEXT, "Directory of file(s)", required=True),
+ Column("filename", TEXT, "Name portion of file path"),
+ Column("inode", BIGINT, "Filesystem inode number"),
+ Column("uid", BIGINT, "Owning user ID"),
+ Column("gid", BIGINT, "Owning group ID"),
+ Column("mode", TEXT, "Permission bits"),
+ Column("device", BIGINT, "Device ID (optional)"),
+ Column("size", BIGINT, "Size of file in bytes"),
+ Column("block_size", INTEGER, "Block size of filesystem"),
+ Column("atime", BIGINT, "Last access time"),
+ Column("mtime", BIGINT, "Last modification time"),
+ Column("ctime", BIGINT, "Last status change time"),
+ Column("btime", BIGINT, "(B)irth or (cr)eate time"),
+ Column("hard_links", INTEGER, "Number of hard links"),
+ Column("symlink", INTEGER, "1 if the path is a symlink, otherwise 0"),
+ Column("type", TEXT, "File status"),
+])
+extended_schema(WINDOWS, [
+ Column("attributes", TEXT, "File attrib string. See: https://ss64.com/nt/attrib.html"),
+ Column("volume_serial", TEXT, "Volume serial number"),
+ Column("file_id", TEXT, "file ID"),
+ Column("product_version", TEXT, "File product version"),
+])
+attributes(utility=True)
+implementation("utility/file@genFile")
+examples([
+ "select * from file where path = '/etc/passwd'",
+ "select * from file where directory = '/etc/'",
+ "select * from file where path LIKE '/etc/%'",
+])
--- /dev/null
+table_name("time")
+description("Track current date and time in the system.")
+schema([
+ Column("weekday", TEXT, "Current weekday in the system"),
+ Column("year", INTEGER, "Current year in the system"),
+ Column("month", INTEGER, "Current month in the system"),
+ Column("day", INTEGER, "Current day in the system"),
+ Column("hour", INTEGER, "Current hour in the system"),
+ Column("minutes", INTEGER, "Current minutes in the system"),
+ Column("seconds", INTEGER, "Current seconds in the system"),
+ Column("timezone", TEXT, "Current timezone in the system"),
+ Column("local_time", INTEGER, "Current local UNIX time in the system",
+ aliases=["localtime"]),
+ Column("local_timezone", TEXT, "Current local timezone in the system"),
+ Column("unix_time", INTEGER,
+ "Current UNIX time in the system, converted to UTC if --utc enabled"),
+ Column("timestamp", TEXT, "Current timestamp (log format) in the system"),
+ Column("datetime", TEXT, "Current date and time (ISO format) in the system",
+ aliases=["date_time"]),
+ Column("iso_8601", TEXT, "Current time (ISO format) in the system"),
+])
+extended_schema(WINDOWS, [
+ Column("win_timestamp", BIGINT, "Timestamp value in 100 nanosecond units."),
+])
+attributes(utility=True)
+implementation("time@genTime")
-/*
- * Copyright (c) 2014, Facebook, Inc.
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
*/
#include <set>
-#include <mutex>
#include <grp.h>
#include <osquery/core.h>
#include <osquery/tables.h>
+#include <osquery/utils/mutex.h>
namespace osquery {
namespace tables {
-std::mutex grpEnumerationMutex;
+Mutex grpEnumerationMutex;
-QueryData genGroups(QueryContext &context) {
- std::lock_guard<std::mutex> lock(grpEnumerationMutex);
+QueryData genGroups(QueryContext& context) {
QueryData results;
- struct group *grp = nullptr;
- std::set<long> groups_in;
+ struct group* grp = nullptr;
- setgrent();
- while ((grp = getgrent()) != nullptr) {
- if (std::find(groups_in.begin(), groups_in.end(), grp->gr_gid) ==
- groups_in.end()) {
+ if (context.constraints["gid"].exists(EQUALS)) {
+ auto gids = context.constraints["gid"].getAll<long long>(EQUALS);
+ for (const auto& gid : gids) {
Row r;
- r["gid"] = INTEGER(grp->gr_gid);
- r["gid_signed"] = INTEGER((int32_t) grp->gr_gid);
- r["groupname"] = TEXT(grp->gr_name);
+ grp = getgrgid(gid);
+ r["gid"] = BIGINT(gid);
+ if (grp != nullptr) {
+ r["gid_signed"] = INTEGER((int32_t)grp->gr_gid);
+ r["groupname"] = TEXT(grp->gr_name);
+ }
results.push_back(r);
- groups_in.insert(grp->gr_gid);
}
+ } else {
+ std::set<long> groups_in;
+ WriteLock lock(grpEnumerationMutex);
+ setgrent();
+ while ((grp = getgrent()) != nullptr) {
+ if (std::find(groups_in.begin(), groups_in.end(), grp->gr_gid) ==
+ groups_in.end()) {
+ Row r;
+ r["gid"] = INTEGER(grp->gr_gid);
+ r["gid_signed"] = INTEGER((int32_t)grp->gr_gid);
+ r["groupname"] = TEXT(grp->gr_name);
+ results.push_back(r);
+ groups_in.insert(grp->gr_gid);
+ }
+ }
+ endgrent();
+ groups_in.clear();
}
- endgrent();
- groups_in.clear();
return results;
}
-/*
- * Copyright (c) 2014, Facebook, Inc.
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
*/
-#include <string>
#include <map>
+#include <string>
#include <stdlib.h>
+#include <sys/stat.h>
#include <unistd.h>
+#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/trim.hpp>
+#include <boost/noncopyable.hpp>
+#include <boost/regex.hpp>
#include <osquery/core.h>
-#include <osquery/tables.h>
#include <osquery/filesystem/filesystem.h>
+#include <osquery/filesystem/linux/proc.h>
+#include <osquery/logger.h>
+#include <osquery/sql/dynamic_table_row.h>
+#include <osquery/tables.h>
+
#include <osquery/utils/conversions/split.h>
+#include <osquery/utils/system/uptime.h>
+
+#include <ctime>
namespace osquery {
namespace tables {
-inline std::string getProcAttr(const std::string& attr, const std::string& pid) {
+const int kMSIn1CLKTCK = (1000 / sysconf(_SC_CLK_TCK));
+
+inline std::string getProcAttr(const std::string& attr,
+ const std::string& pid) {
return "/proc/" + pid + "/" + attr;
}
return content;
}
-inline std::string readProcLink(const std::string& attr, const std::string& pid) {
+inline std::string readProcLink(const std::string& attr,
+ const std::string& pid) {
// The exe is a symlink to the binary on-disk.
auto attr_path = getProcAttr(attr, pid);
- std::string result;
- char link_path[PATH_MAX] = {0};
- auto bytes = readlink(attr_path.c_str(), link_path, sizeof(link_path) - 1);
- if (bytes >= 0) {
- result = std::string(link_path);
+ std::string result = "";
+ struct stat sb;
+ if (lstat(attr_path.c_str(), &sb) != -1) {
+ // Some symlinks may report 'st_size' as zero
+ // Use PATH_MAX as best guess
+ // For cases when 'st_size' is not zero but smaller than
+ // PATH_MAX we will still use PATH_MAX to minimize chance
+ // of output trucation during race condition
+ ssize_t buf_size = sb.st_size < PATH_MAX ? PATH_MAX : sb.st_size;
+ // +1 for \0, since readlink does not append a null
+ char* linkname = static_cast<char*>(malloc(buf_size + 1));
+ ssize_t r = readlink(attr_path.c_str(), linkname, buf_size);
+
+ if (r > 0) { // Success check
+ // r may not be equal to buf_size
+ // if r == buf_size there was race condition
+ // and link is longer than buf_size and because of this
+ // truncated
+ linkname[r] = '\0';
+ result = std::string(linkname);
+ }
+ free(linkname);
}
return result;
}
+// In the case where the linked binary path ends in " (deleted)", and a file
+// actually exists at that path, check whether the inode of that file matches
+// the inode of the mapped file in /proc/%pid/maps
+Status deletedMatchesInode(const std::string& path, const std::string& pid) {
+ const std::string maps_path = getProcAttr("maps", pid);
+ std::string maps_contents;
+ auto s = osquery::readFile(maps_path, maps_contents);
+ if (!s.ok()) {
+ return Status(-1, "Cannot read maps file: " + maps_path);
+ }
+
+ // Extract the expected inode of the binary file from /proc/%pid/maps
+ boost::smatch what;
+ boost::regex expression("([0-9]+)\\h+\\Q" + path + "\\E");
+ if (!boost::regex_search(maps_contents, what, expression)) {
+ return Status(-1, "Could not find binary inode in maps file: " + maps_path);
+ }
+ std::string inode = what[1];
+
+ // stat the file at the expected binary path
+ struct stat st;
+ if (stat(path.c_str(), &st) != 0) {
+ return Status(-1, "Error in stat of binary: " + path);
+ }
+
+ // If the inodes match, the binary name actually ends with " (deleted)"
+ if (std::to_string(st.st_ino) == inode) {
+ return Status::success();
+ } else {
+ return Status(1, "Inodes do not match");
+ }
+}
+
std::set<std::string> getProcList(const QueryContext& context) {
std::set<std::string> pidlist;
if (context.constraints.count("pid") > 0 &&
readFile(map, content);
for (auto& line : osquery::split(content, "\n")) {
auto fields = osquery::split(line, " ");
-
- Row r;
- r["pid"] = pid;
-
// If can't read address, not sure.
if (fields.size() < 5) {
continue;
}
- if (fields[0].size() > 0) {
+ Row r;
+ r["pid"] = pid;
+ if (!fields[0].empty()) {
auto addresses = osquery::split(fields[0], "-");
- r["start"] = "0x" + addresses[0];
- r["end"] = "0x" + addresses[1];
+ if (addresses.size() >= 2) {
+ r["start"] = "0x" + addresses[0];
+ r["end"] = "0x" + addresses[1];
+ } else {
+ // Problem with the address format.
+ continue;
+ }
}
r["permissions"] = fields[1];
- r["offset"] = BIGINT(std::stoll(fields[2], nullptr, 16));
+ auto offset = tryTo<long long>(fields[2], 16);
+ r["offset"] = BIGINT((offset) ? offset.take() : -1);
r["device"] = fields[3];
r["inode"] = fields[4];
}
// BSS with name in pathname.
- r["pseudo"] = (fields[4] == "0" && r["path"].size() > 0) ? "1" : "0";
- results.push_back(r);
+ r["pseudo"] = (fields[4] == "0" && !r["path"].empty()) ? "1" : "0";
+ results.push_back(std::move(r));
}
}
-struct SimpleProcStat {
- // Output from string parsing /proc/<pid>/status.
- std::string parent; // PPid:
- std::string name; // Name:
- std::string real_uid; // Uid: * - - -
- std::string real_gid; // Gid: * - - -
- std::string effective_uid; // Uid: - * - -
- std::string effective_gid; // Gid: - * - -
-
- std::string resident_size; // VmRSS:
- std::string phys_footprint; // VmSize:
-
- // Output from sring parsing /proc/<pid>/stat.
+/**
+ * Output from string parsing /proc/<pid>/status.
+ */
+struct SimpleProcStat : private boost::noncopyable {
+ public:
+ std::string name;
+ std::string real_uid;
+ std::string real_gid;
+ std::string effective_uid;
+ std::string effective_gid;
+ std::string saved_uid;
+ std::string saved_gid;
+ std::string resident_size;
+ std::string total_size;
+ std::string state;
+ std::string parent;
+ std::string group;
+ std::string nice;
+ std::string threads;
std::string user_time;
std::string system_time;
std::string start_time;
+
+ /// For errors processing proc data.
+ Status status;
+
+ explicit SimpleProcStat(const std::string& pid);
};
-SimpleProcStat getProcStat(const std::string& pid) {
- SimpleProcStat stat;
+SimpleProcStat::SimpleProcStat(const std::string& pid) {
std::string content;
if (readFile(getProcAttr("stat", pid), content).ok()) {
- auto detail_start = content.find_last_of(")");
+ auto start = content.find_last_of(")");
// Start parsing stats from ") <MODE>..."
- auto details = osquery::split(content.substr(detail_start + 2), " ");
- stat.parent = details.at(1);
- stat.user_time = details.at(11);
- stat.system_time = details.at(12);
- stat.start_time = details.at(19);
+ if (start == std::string::npos || content.size() <= start + 2) {
+ status = Status(1, "Invalid /proc/stat header");
+ return;
+ }
+
+ auto details = osquery::split(content.substr(start + 2), " ");
+ if (details.size() <= 19) {
+ status = Status(1, "Invalid /proc/stat content");
+ return;
+ }
+
+ this->state = details.at(0);
+ this->parent = details.at(1);
+ this->group = details.at(2);
+ this->user_time = details.at(11);
+ this->system_time = details.at(12);
+ this->nice = details.at(16);
+ this->threads = details.at(17);
+ this->start_time = details.at(19);
}
- if (readFile(getProcAttr("status", pid), content).ok()) {
- for (const auto& line : osquery::split(content, "\n")) {
- // Status lines are formatted: Key: Value....\n.
- auto detail = osquery::split(line, ':', 1);
- if (detail.size() != 2) {
- continue;
- }
+ // /proc/N/status may be not available, or readable by this user.
+ if (!readFile(getProcAttr("status", pid), content).ok()) {
+ status = Status(1, "Cannot read /proc/status");
+ return;
+ }
+
+ for (const auto& line : osquery::split(content, "\n")) {
+ // Status lines are formatted: Key: Value....\n.
+ auto detail = osquery::split(line, ':', 1);
+ if (detail.size() != 2) {
+ continue;
+ }
- // There are specific fields from each detail.
- if (detail.at(0) == "Name") {
- stat.name = detail.at(1);
- } else if (detail.at(0) == "VmRSS") {
- detail[1].erase(detail.at(1).end() - 3, detail.at(1).end());
- // Memory is reported in kB.
- stat.resident_size = detail.at(1) + "000";
- } else if (detail.at(0) == "VmSize") {
- detail[1].erase(detail.at(1).end() - 3, detail.at(1).end());
- // Memory is reported in kB.
- stat.phys_footprint = detail.at(1) + "000";
- } else if (detail.at(0) == "Gid") {
- // Format is: R E - -
- auto gid_detail = osquery::split(detail.at(1), "\t");
- if (gid_detail.size() == 4) {
- stat.real_gid = gid_detail.at(0);
- stat.effective_gid = gid_detail.at(1);
- }
- } else if (detail.at(0) == "Uid") {
- auto uid_detail = osquery::split(detail.at(1), "\t");
- if (uid_detail.size() == 4) {
- stat.real_uid = uid_detail.at(0);
- stat.effective_uid = uid_detail.at(1);
- }
+ // There are specific fields from each detail.
+ if (detail.at(0) == "Name") {
+ this->name = detail.at(1);
+ } else if (detail.at(0) == "VmRSS") {
+ detail[1].erase(detail.at(1).end() - 3, detail.at(1).end());
+ // Memory is reported in kB.
+ this->resident_size = detail.at(1) + "000";
+ } else if (detail.at(0) == "VmSize") {
+ detail[1].erase(detail.at(1).end() - 3, detail.at(1).end());
+ // Memory is reported in kB.
+ this->total_size = detail.at(1) + "000";
+ } else if (detail.at(0) == "Gid") {
+ // Format is: R E - -
+ auto gid_detail = osquery::split(detail.at(1), "\t");
+ if (gid_detail.size() == 4) {
+ this->real_gid = gid_detail.at(0);
+ this->effective_gid = gid_detail.at(1);
+ this->saved_gid = gid_detail.at(2);
+ }
+ } else if (detail.at(0) == "Uid") {
+ auto uid_detail = osquery::split(detail.at(1), "\t");
+ if (uid_detail.size() == 4) {
+ this->real_uid = uid_detail.at(0);
+ this->effective_uid = uid_detail.at(1);
+ this->saved_uid = uid_detail.at(2);
}
}
}
+}
+
+/**
+ * Output from string parsing /proc/<pid>/io.
+ */
+struct SimpleProcIo : private boost::noncopyable {
+ public:
+ std::string read_bytes;
+ std::string write_bytes;
+ std::string cancelled_write_bytes;
+
+ /// For errors processing proc data.
+ Status status;
+
+ explicit SimpleProcIo(const std::string& pid);
+};
+
+SimpleProcIo::SimpleProcIo(const std::string& pid) {
+ std::string content;
+ if (!readFile(getProcAttr("io", pid), content).ok()) {
+ status = Status(
+ 1, "Cannot read /proc/" + pid + "/io (is osquery running as root?)");
+ return;
+ }
+
+ for (const auto& line : osquery::split(content, "\n")) {
+ // IO lines are formatted: Key: Value....\n.
+ auto detail = osquery::split(line, ':', 1);
+ if (detail.size() != 2) {
+ continue;
+ }
+
+ // There are specific fields from each detail
+ if (detail.at(0) == "read_bytes") {
+ this->read_bytes = detail.at(1);
+ } else if (detail.at(0) == "write_bytes") {
+ this->write_bytes = detail.at(1);
+ } else if (detail.at(0) == "cancelled_write_bytes") {
+ this->cancelled_write_bytes = detail.at(1);
+ }
+ }
+}
+
+/**
+ * @brief Determine if the process path (binary) exists on the filesystem.
+ *
+ * If the path of the executable that started the process is available and
+ * the path exists on disk, set on_disk to 1. If the path is not
+ * available, set on_disk to -1. If, and only if, the path of the
+ * executable is available and the file does NOT exist on disk, set on_disk
+ * to 0.
+ *
+ * @param pid The string (because we're referencing file path) pid.
+ * @param path A mutable string found from /proc/N/exe. If this is found
+ * to contain the (deleted) suffix, it will be removed.
+ * @return A tristate -1 error, 1 yes, 0 nope.
+ */
+int getOnDisk(const std::string& pid, std::string& path) {
+ if (path.empty()) {
+ return -1;
+ }
+
+ // The string appended to the exe path when the binary is deleted
+ const std::string kDeletedString = " (deleted)";
+ if (!boost::algorithm::ends_with(path, kDeletedString)) {
+ return (osquery::pathExists(path)) ? 1 : 0;
+ }
- return stat;
+ if (!osquery::pathExists(path)) {
+ // No file exists with the path including " (deleted)", so we can strip
+ // this from the path and set on_disk = 0
+ path.erase(path.size() - kDeletedString.size());
+ return 0;
+ }
+
+ // Special case in which we have to check the inode to see whether the
+ // process is actually running from a binary file ending with
+ // " (deleted)". See #1607
+ std::string maps_contents;
+ Status deleted = deletedMatchesInode(path, pid);
+ if (deleted.getCode() == -1) {
+ LOG(ERROR) << deleted.getMessage();
+ return -1;
+ } else if (deleted.getCode() == 0) {
+ // The process is actually running from a binary ending with
+ // " (deleted)"
+ return 1;
+ } else {
+ // There is a collision with a file name ending in " (deleted)", but
+ // that file is not the binary for this process
+ path.erase(path.size() - kDeletedString.size());
+ return 0;
+ }
}
-void genProcess(const std::string& pid, QueryData& results) {
+void genProcess(const std::string& pid,
+ long system_boot_time,
+ TableRows& results) {
// Parse the process stat and status.
- auto proc_stat = getProcStat(pid);
+ SimpleProcStat proc_stat(pid);
+ // Parse the process io
+ SimpleProcIo proc_io(pid);
- Row r;
+ if (!proc_stat.status.ok()) {
+ VLOG(1) << proc_stat.status.getMessage() << " for pid " << pid;
+ return;
+ }
+
+ auto r = make_table_row();
r["pid"] = pid;
r["parent"] = proc_stat.parent;
r["path"] = readProcLink("exe", pid);
r["name"] = proc_stat.name;
-
+ r["pgroup"] = proc_stat.group;
+ r["state"] = proc_stat.state;
+ r["nice"] = proc_stat.nice;
+ r["threads"] = proc_stat.threads;
// Read/parse cmdline arguments.
r["cmdline"] = readProcCMDLine(pid);
r["cwd"] = readProcLink("cwd", pid);
r["root"] = readProcLink("root", pid);
-
r["uid"] = proc_stat.real_uid;
r["euid"] = proc_stat.effective_uid;
+ r["suid"] = proc_stat.saved_uid;
r["gid"] = proc_stat.real_gid;
r["egid"] = proc_stat.effective_gid;
+ r["sgid"] = proc_stat.saved_gid;
- // If the path of the executable that started the process is available and
- // the path exists on disk, set on_disk to 1. If the path is not
- // available, set on_disk to -1. If, and only if, the path of the
- // executable is available and the file does NOT exist on disk, set on_disk
- // to 0.
- r["on_disk"] = osquery::pathExists(r["path"]).toString();
+ r["on_disk"] = INTEGER(getOnDisk(pid, r["path"]));
// size/memory information
r["wired_size"] = "0"; // No support for unpagable counters in linux.
r["resident_size"] = proc_stat.resident_size;
- r["phys_footprint"] = proc_stat.phys_footprint;
+ r["total_size"] = proc_stat.total_size;
// time information
- r["user_time"] = proc_stat.user_time;
- r["system_time"] = proc_stat.system_time;
- r["start_time"] = proc_stat.start_time;
+ auto usr_time = std::strtoull(proc_stat.user_time.data(), nullptr, 10);
+ r["user_time"] = std::to_string(usr_time * kMSIn1CLKTCK);
+ auto sys_time = std::strtoull(proc_stat.system_time.data(), nullptr, 10);
+ r["system_time"] = std::to_string(sys_time * kMSIn1CLKTCK);
+
+ auto proc_start_time_exp = tryTo<long>(proc_stat.start_time);
+ if (proc_start_time_exp.isValue() && system_boot_time > 0) {
+ r["start_time"] = INTEGER(system_boot_time + proc_start_time_exp.take() /
+ sysconf(_SC_CLK_TCK));
+ } else {
+ r["start_time"] = "-1";
+ }
+
+ if (!proc_io.status.ok()) {
+ // /proc/<pid>/io can require root to access, so don't fail if we can't
+ VLOG(1) << proc_io.status.getMessage();
+ } else {
+ r["disk_bytes_read"] = proc_io.read_bytes;
+ long long write_bytes = tryTo<long long>(proc_io.write_bytes).takeOr(0ll);
+ long long cancelled_write_bytes =
+ tryTo<long long>(proc_io.cancelled_write_bytes).takeOr(0ll);
+
+ r["disk_bytes_written"] =
+ std::to_string(write_bytes - cancelled_write_bytes);
+ }
results.push_back(r);
}
-QueryData genProcesses(QueryContext& context) {
- QueryData results;
+void genNamespaces(const std::string& pid, QueryData& results) {
+ Row r;
+
+ ProcessNamespaceList proc_ns;
+ Status status = procGetProcessNamespaces(pid, proc_ns);
+ if (!status.ok()) {
+ VLOG(1) << "Namespaces for pid " << pid
+ << " are incomplete: " << status.what();
+ }
+
+ r["pid"] = pid;
+ for (const auto& pair : proc_ns) {
+ r[pair.first + "_namespace"] = std::to_string(pair.second);
+ }
+
+ results.push_back(r);
+}
+
+TableRows genProcesses(QueryContext& context) {
+ TableRows results;
+ auto system_boot_time = getUptime();
+ if (system_boot_time > 0) {
+ system_boot_time = std::time(nullptr) - system_boot_time;
+ }
auto pidlist = getProcList(context);
for (const auto& pid : pidlist) {
- genProcess(pid, results);
+ genProcess(pid, system_boot_time, results);
}
return results;
return results;
}
+
+QueryData genProcessNamespaces(QueryContext& context) {
+ QueryData results;
+
+ const auto pidlist = getProcList(context);
+ for (const auto& pid : pidlist) {
+ genNamespaces(pid, results);
+ }
+
+ return results;
+}
}
}
-/*
- * Copyright (c) 2014, Facebook, Inc.
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
*/
-#include <set>
-#include <mutex>
-#include <vector>
-#include <string>
-
#include <pwd.h>
+#include <mutex>
+
#include <osquery/core.h>
#include <osquery/tables.h>
-#include <osquery/status.h>
-#include <osquery/logger.h>
+#include <osquery/utils/mutex.h>
+#include <osquery/utils/conversions/tryto.h>
namespace osquery {
namespace tables {
-std::mutex pwdEnumerationMutex;
+Mutex pwdEnumerationMutex;
+
+void genUser(const struct passwd* pwd, QueryData& results) {
+ Row r;
+ r["uid"] = BIGINT(pwd->pw_uid);
+ r["gid"] = BIGINT(pwd->pw_gid);
+ r["uid_signed"] = BIGINT((int32_t)pwd->pw_uid);
+ r["gid_signed"] = BIGINT((int32_t)pwd->pw_gid);
+
+ if (pwd->pw_name != nullptr) {
+ r["username"] = TEXT(pwd->pw_name);
+ }
+
+ if (pwd->pw_gecos != nullptr) {
+ r["description"] = TEXT(pwd->pw_gecos);
+ }
+
+ if (pwd->pw_dir != nullptr) {
+ r["directory"] = TEXT(pwd->pw_dir);
+ }
+
+ if (pwd->pw_shell != nullptr) {
+ r["shell"] = TEXT(pwd->pw_shell);
+ }
+ results.push_back(r);
+}
QueryData genUsers(QueryContext& context) {
- std::lock_guard<std::mutex> lock(pwdEnumerationMutex);
QueryData results;
- struct passwd *pwd = nullptr;
- std::set<long> users_in;
- while ((pwd = getpwent()) != nullptr) {
- if (std::find(users_in.begin(), users_in.end(), pwd->pw_uid) ==
- users_in.end()) {
- Row r;
- r["uid"] = BIGINT(pwd->pw_uid);
- r["gid"] = BIGINT(pwd->pw_gid);
- r["uid_signed"] = BIGINT((int32_t) pwd->pw_uid);
- r["gid_signed"] = BIGINT((int32_t) pwd->pw_gid);
- r["username"] = TEXT(pwd->pw_name);
- r["description"] = TEXT(pwd->pw_gecos);
- r["directory"] = TEXT(pwd->pw_dir);
- r["shell"] = TEXT(pwd->pw_shell);
- results.push_back(r);
- users_in.insert(pwd->pw_uid);
+ struct passwd* pwd = nullptr;
+ if (context.constraints["uid"].exists(EQUALS)) {
+ auto uids = context.constraints["uid"].getAll(EQUALS);
+ for (const auto& uid : uids) {
+ auto const auid_exp = tryTo<long>(uid, 10);
+ if (auid_exp.isValue()) {
+ WriteLock lock(pwdEnumerationMutex);
+ pwd = getpwuid(auid_exp.get());
+ if (pwd != nullptr) {
+ genUser(pwd, results);
+ }
+ }
}
+ } else if (context.constraints["username"].exists(EQUALS)) {
+ auto usernames = context.constraints["username"].getAll(EQUALS);
+ for (const auto& username : usernames) {
+ WriteLock lock(pwdEnumerationMutex);
+ pwd = getpwnam(username.c_str());
+ if (pwd != nullptr) {
+ genUser(pwd, results);
+ }
+ }
+ } else {
+ WriteLock lock(pwdEnumerationMutex);
+ pwd = getpwent();
+ while (pwd != nullptr) {
+ genUser(pwd, results);
+ pwd = getpwent();
+ }
+ endpwent();
}
- endpwent();
- users_in.clear();
return results;
}
-
-/// Example of update feature
-Status updateUsers(Row& row) {
- for (auto& r : row)
- LOG(ERROR) << "DEBUG: " << r.first << ", " << r.second;
-
- return Status(0, "OK");
-}
}
}
--- /dev/null
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
+ */
+
+#if !defined(WIN32)
+#include <sys/stat.h>
+#endif
+
+#include <osquery/filesystem/filesystem.h>
+#include <osquery/logger.h>
+#include <osquery/tables.h>
+#include <osquery/filesystem/fileops.h>
+
+namespace fs = boost::filesystem;
+
+namespace osquery {
+
+namespace tables {
+
+#if !defined(WIN32)
+
+const std::map<fs::file_type, std::string> kTypeNames{
+ {fs::regular_file, "regular"},
+ {fs::directory_file, "directory"},
+ {fs::symlink_file, "symlink"},
+ {fs::block_file, "block"},
+ {fs::character_file, "character"},
+ {fs::fifo_file, "fifo"},
+ {fs::socket_file, "socket"},
+ {fs::type_unknown, "unknown"},
+ {fs::status_error, "error"},
+};
+
+#endif
+
+void genFileInfo(const fs::path& path,
+ const fs::path& parent,
+ const std::string& pattern,
+ QueryData& results) {
+ // Must provide the path, filename, directory separate from boost path->string
+ // helpers to match any explicit (query-parsed) predicate constraints.
+
+ Row r;
+ r["path"] = path.string();
+ r["filename"] = path.filename().string();
+ r["directory"] = parent.string();
+ r["symlink"] = "0";
+
+#if !defined(WIN32)
+
+ struct stat file_stat;
+
+ // On POSIX systems, first check the link state.
+ struct stat link_stat;
+ if (lstat(path.string().c_str(), &link_stat) < 0) {
+ // Path was not real, had too may links, or could not be accessed.
+ return;
+ }
+ if (S_ISLNK(link_stat.st_mode)) {
+ r["symlink"] = "1";
+ }
+
+ if (stat(path.string().c_str(), &file_stat)) {
+ file_stat = link_stat;
+ }
+
+ r["inode"] = BIGINT(file_stat.st_ino);
+ r["uid"] = BIGINT(file_stat.st_uid);
+ r["gid"] = BIGINT(file_stat.st_gid);
+ r["mode"] = lsperms(file_stat.st_mode);
+ r["device"] = BIGINT(file_stat.st_rdev);
+ r["size"] = BIGINT(file_stat.st_size);
+ r["block_size"] = INTEGER(file_stat.st_blksize);
+ r["hard_links"] = INTEGER(file_stat.st_nlink);
+
+ r["atime"] = BIGINT(file_stat.st_atime);
+ r["mtime"] = BIGINT(file_stat.st_mtime);
+ r["ctime"] = BIGINT(file_stat.st_ctime);
+
+#if defined(__linux__)
+ // No 'birth' or create time in Linux or Windows.
+ r["btime"] = "0";
+#else
+ r["btime"] = BIGINT(file_stat.st_birthtimespec.tv_sec);
+#endif
+
+ // Type booleans
+ boost::system::error_code ec;
+ auto status = fs::status(path, ec);
+ if (kTypeNames.count(status.type())) {
+ r["type"] = kTypeNames.at(status.type());
+ } else {
+ r["type"] = "unknown";
+ }
+
+#else
+
+ WINDOWS_STAT file_stat;
+
+ auto rtn = platformStat(path, &file_stat);
+ if (!rtn.ok()) {
+ VLOG(1) << "PlatformStat failed with " << rtn.getMessage();
+ return;
+ }
+
+ r["symlink"] = INTEGER(file_stat.symlink);
+ r["inode"] = BIGINT(file_stat.inode);
+ r["uid"] = BIGINT(file_stat.uid);
+ r["gid"] = BIGINT(file_stat.gid);
+ r["mode"] = TEXT(file_stat.mode);
+ r["device"] = BIGINT(file_stat.device);
+ r["size"] = BIGINT(file_stat.size);
+ r["block_size"] = INTEGER(file_stat.block_size);
+ r["hard_links"] = INTEGER(file_stat.hard_links);
+ r["atime"] = BIGINT(file_stat.atime);
+ r["mtime"] = BIGINT(file_stat.mtime);
+ r["ctime"] = BIGINT(file_stat.ctime);
+ r["btime"] = BIGINT(file_stat.btime);
+ r["type"] = TEXT(file_stat.type);
+ r["attributes"] = TEXT(file_stat.attributes);
+ r["file_id"] = TEXT(file_stat.file_id);
+ r["volume_serial"] = TEXT(file_stat.volume_serial);
+ r["product_version"] = TEXT(file_stat.product_version);
+
+#endif
+
+ results.push_back(r);
+}
+
+QueryData genFile(QueryContext& context) {
+ QueryData results;
+
+ // Resolve file paths for EQUALS and LIKE operations.
+ auto paths = context.constraints["path"].getAll(EQUALS);
+ context.expandConstraints(
+ "path",
+ LIKE,
+ paths,
+ ([&](const std::string& pattern, std::set<std::string>& out) {
+ std::vector<std::string> patterns;
+ auto status =
+ resolveFilePattern(pattern, patterns, GLOB_ALL | GLOB_NO_CANON);
+ if (status.ok()) {
+ for (const auto& resolved : patterns) {
+ out.insert(resolved);
+ }
+ }
+ return status;
+ }));
+
+ // Iterate through each of the resolved/supplied paths.
+ for (const auto& path_string : paths) {
+ fs::path path = path_string;
+ genFileInfo(path, path.parent_path(), "", results);
+ }
+
+ // Resolve directories for EQUALS and LIKE operations.
+ auto directories = context.constraints["directory"].getAll(EQUALS);
+ context.expandConstraints(
+ "directory",
+ LIKE,
+ directories,
+ ([&](const std::string& pattern, std::set<std::string>& out) {
+ std::vector<std::string> patterns;
+ auto status =
+ resolveFilePattern(pattern, patterns, GLOB_FOLDERS | GLOB_NO_CANON);
+ if (status.ok()) {
+ for (const auto& resolved : patterns) {
+ out.insert(resolved);
+ }
+ }
+ return status;
+ }));
+
+ // Now loop through constraints using the directory column constraint.
+ for (const auto& directory_string : directories) {
+ if (!isReadable(directory_string) || !isDirectory(directory_string)) {
+ continue;
+ }
+
+ try {
+ // Iterate over the directory and generate info for each regular file.
+ fs::directory_iterator begin(directory_string), end;
+ for (; begin != end; ++begin) {
+ genFileInfo(begin->path(), directory_string, "", results);
+ }
+ } catch (const fs::filesystem_error& /* e */) {
+ continue;
+ }
+ }
+
+ return results;
+}
+}
+} // namespace osquery
--- /dev/null
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
+ */
+
+#include <ctime>
+
+#include <boost/algorithm/string/trim.hpp>
+
+#include <osquery/utils/system/time.h>
+
+#include <osquery/flags.h>
+#include <osquery/system.h>
+#include <osquery/tables.h>
+
+namespace osquery {
+
+DECLARE_bool(utc);
+
+namespace tables {
+
+QueryData genTime(QueryContext& context) {
+ Row r;
+ time_t local_time = getUnixTime();
+ auto osquery_time = getUnixTime();
+ auto osquery_timestamp = getAsciiTime();
+
+ // The concept of 'now' is configurable.
+ struct tm gmt;
+ gmtime_r(&local_time, &gmt);
+
+ struct tm now;
+ if (FLAGS_utc) {
+ now = gmt;
+ } else {
+ localtime_r(&local_time, &now);
+ }
+
+ struct tm local;
+ localtime_r(&local_time, &local);
+ local_time = std::mktime(&local);
+
+ char weekday[10] = {0};
+ strftime(weekday, sizeof(weekday), "%A", &now);
+
+ char timezone[5] = {0};
+ strftime(timezone, sizeof(timezone), "%Z", &now);
+
+ char local_timezone[5] = {0};
+ strftime(local_timezone, sizeof(local_timezone), "%Z", &local);
+
+ char iso_8601[21] = {0};
+ strftime(iso_8601, sizeof(iso_8601), "%FT%TZ", &gmt);
+#ifdef WIN32
+ if (context.isColumnUsed("win_timestamp")) {
+ FILETIME ft = {0};
+ GetSystemTimeAsFileTime(&ft);
+ LARGE_INTEGER li = {0};
+ li.LowPart = ft.dwLowDateTime;
+ li.HighPart = ft.dwHighDateTime;
+ long long int hns = li.QuadPart;
+ r["win_timestamp"] = BIGINT(hns);
+ }
+#endif
+ r["weekday"] = SQL_TEXT(weekday);
+ r["year"] = INTEGER(now.tm_year + 1900);
+ r["month"] = INTEGER(now.tm_mon + 1);
+ r["day"] = INTEGER(now.tm_mday);
+ r["hour"] = INTEGER(now.tm_hour);
+ r["minutes"] = INTEGER(now.tm_min);
+ r["seconds"] = INTEGER(now.tm_sec);
+ r["timezone"] = SQL_TEXT(timezone);
+ if (r["timezone"].empty()) {
+ r["timezone"] = "UTC";
+ }
+
+ r["local_time"] = INTEGER(local_time);
+ r["local_timezone"] = SQL_TEXT(local_timezone);
+ if (r["local_timezone"].empty()) {
+ r["local_timezone"] = "UTC";
+ }
+
+ r["unix_time"] = INTEGER(osquery_time);
+ r["timestamp"] = SQL_TEXT(osquery_timestamp);
+ // Date time is provided in ISO 8601 format, then duplicated in iso_8601.
+ r["datetime"] = SQL_TEXT(iso_8601);
+ r["iso_8601"] = SQL_TEXT(iso_8601);
+
+ QueryData results;
+ results.push_back(r);
+ return results;
+}
+} // namespace tables
+} // namespace osquery
#!/usr/bin/env python
-# Copyright (c) 2014, Facebook, Inc.
+# Copyright (c) 2014-present, Facebook, Inc.
# All rights reserved.
#
-# This source code is licensed under the BSD-style license found in the
-# LICENSE file in the root directory of this source tree. An additional grant
-# of patent rights can be found in the PATENTS file in the same directory.
+# This source code is licensed in accordance with the terms specified in
+# the LICENSE file found in the root directory of this source tree.
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
+import argparse
import jinja2
import os
import sys
-OUTPUT_NAME = "amalgamation.cpp"
+TEMPLATE_NAME = "amalgamation.cpp.in"
BEGIN_LINE = "/// BEGIN[GENTABLE]"
END_LINE = "/// END[GENTABLE]"
-def usage(progname):
- """ print program usage """
- print("Usage: %s /path/to/generated/tables output.cpp " % progname)
- return 1
-
-
def genTableData(filename):
with open(filename, "rU") as fh:
data = fh.read()
def main(argc, argv):
- if argc < 3:
- return usage(argv[0])
-
- specs = argv[1]
- directory = argv[2]
+ parser = argparse.ArgumentParser(
+ "Generate C++ amalgamation from C++ Table Plugin targets")
+ parser.add_argument("--foreign", default=False, action="store_true",
+ help="Generate a foreign table set amalgamation")
+ parser.add_argument("--templates",
+ help="Path to codegen output .cpp.in templates")
+ parser.add_argument("--category", help="Category name of generated tables")
+ parser.add_argument("--sources",
+ help="Path to the folder containing the .cpp files")
+ parser.add_argument("--output", help="Path to the output .cpp files")
+ args = parser.parse_args()
tables = []
- template = os.path.join(specs, "templates", "%s.in" % OUTPUT_NAME)
+ # Discover the output template, usually a black cpp file with includes.
+ template = os.path.join(args.templates, TEMPLATE_NAME)
with open(template, "rU") as fh:
template_data = fh.read()
- for base, dirnames, filenames in os.walk(directory):
+ for base, _, filenames in os.walk(args.sources):
for filename in filenames:
- if filename == OUTPUT_NAME:
+ if filename == args.category:
continue
table_data = genTableData(os.path.join(base, filename))
if table_data is not None:
tables.append(table_data)
- amalgamation = jinja2.Template(template_data).render(
- tables=tables)
- output = os.path.join(directory, OUTPUT_NAME)
- with open(output, "w") as fh:
+ env = jinja2.Environment(keep_trailing_newline=True)
+ amalgamation = env.from_string(template_data).render(tables=tables,
+ foreign=args.foreign)
+ try:
+ os.makedirs(os.path.dirname(args.output))
+ except OSError:
+ # Generated folder already exists
+ pass
+ with open(args.output, "w") as fh:
fh.write(amalgamation)
return 0
#!/usr/bin/env python
-# Copyright (c) 2014, Facebook, Inc.
+# Copyright (c) 2014-present, Facebook, Inc.
# All rights reserved.
#
-# This source code is licensed under the BSD-style license found in the
-# LICENSE file in the root directory of this source tree. An additional grant
-# of patent rights can be found in the PATENTS file in the same directory.
+# This source code is licensed in accordance with the terms specified in
+# the LICENSE file found in the root directory of this source tree.
from __future__ import absolute_import
from __future__ import division
import argparse
import ast
+import fnmatch
import jinja2
import logging
import os
import sys
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
-sys.path.append(SCRIPT_DIR + "/../tests")
# the log format for the logging module
LOG_FORMAT = "%(levelname)s [Line %(lineno)d]: %(message)s"
# Temporary reserved column names
RESERVED = ["n", "index"]
-# Supported SQL types for spec
-
+# Set the platform in osquery-language
+PLATFORM = "linux"
+# Supported SQL types for spec
class DataType(object):
-
def __init__(self, affinity, cpp_type="std::string"):
'''A column datatype is a pair of a SQL affinity to C++ type.'''
self.affinity = affinity
return self.affinity
# Define column-type MACROs for the table specs
-TEXT = DataType("TEXT")
-DATE = DataType("TEXT")
-DATETIME = DataType("TEXT")
-INTEGER = DataType("INTEGER", "int")
-BIGINT = DataType("BIGINT", "long long int")
-UNSIGNED_BIGINT = DataType("UNSIGNED_BIGINT", "long long unsigned int")
-DOUBLE = DataType("DOUBLE", "double")
+TEXT = DataType("TEXT_TYPE")
+DATE = DataType("TEXT_TYPE")
+DATETIME = DataType("TEXT_TYPE")
+INTEGER = DataType("INTEGER_TYPE", "int")
+BIGINT = DataType("BIGINT_TYPE", "long long int")
+UNSIGNED_BIGINT = DataType("UNSIGNED_BIGINT_TYPE", "long long unsigned int")
+DOUBLE = DataType("DOUBLE_TYPE", "double")
+BLOB = DataType("BLOB_TYPE", "Blob")
# Define table-category MACROS from the table specs
UNKNOWN = "UNKNOWN"
EVENTS = "EVENTS"
APPLICATION = "APPLICATION"
-def usage():
- """ print program usage """
- print(
- "Usage: %s <spec.table> <file.cpp> [disable_blacklist]" % sys.argv[0])
+# This should mimic the C++ enumeration ColumnOptions in table.h
+COLUMN_OPTIONS = {
+ "index": "INDEX",
+ "additional": "ADDITIONAL",
+ "required": "REQUIRED",
+ "optimized": "OPTIMIZED",
+ "hidden": "HIDDEN",
+}
+
+# Column options that render tables uncacheable.
+NON_CACHEABLE = [
+ "REQUIRED",
+ "ADDITIONAL",
+ "OPTIMIZED",
+]
+
+TABLE_ATTRIBUTES = {
+ "event_subscriber": "EVENT_BASED",
+ "user_data": "USER_BASED",
+ "cacheable": "CACHEABLE",
+ "utility": "UTILITY",
+ "kernel_required": "KERNEL_REQUIRED",
+}
+
+
+def WINDOWS():
+ return PLATFORM in ['windows', 'win32', 'cygwin']
+
+
+def LINUX():
+ return PLATFORM in ['linux']
+
+
+def POSIX():
+ return PLATFORM in ['linux', 'darwin', 'freebsd']
+
+
+def DARWIN():
+ return PLATFORM in ['darwin']
+
+
+def FREEBSD():
+ return PLATFORM in ['freebsd']
def to_camel_case(snake_case):
components = snake_case.split('_')
return components[0] + "".join(x.title() for x in components[1:])
+def to_upper_camel_case(snake_case):
+ """ convert a snake_case string to UpperCamelCase """
+ components = snake_case.split('_')
+ return "".join(x.title() for x in components)
def lightred(msg):
return "\033[1;31m %s \033[0m" % str(msg)
if blacklist is None:
specs_path = os.path.dirname(path)
if os.path.basename(specs_path) != "specs":
- specs_path = os.path.basename(specs_path)
+ specs_path = os.path.dirname(specs_path)
blacklist_path = os.path.join(specs_path, "blacklist")
if not os.path.exists(blacklist_path):
return False
except:
# Blacklist is not readable.
return False
- # table_name based blacklisting!
- return table_name in blacklist if blacklist else False
+ if not blacklist:
+ return False
+ # table_name based blacklisting!
+ for item in blacklist:
+ item = item.split(":")
+ # If this item is restricted to a platform and the platform
+ # and table name match
+ if len(item) > 1 and PLATFORM == item[0] and table_name == item[1]:
+ return True
+ elif len(item) == 1 and table_name == item[0]:
+ return True
+ return False
def setup_templates(templates_path):
if not os.path.exists(templates_path):
- templates_path = os.path.join(os.path.dirname(tables_path), "templates")
+ templates_path = os.path.join(
+ os.path.dirname(tables_path), "templates")
if not os.path.exists(templates_path):
- print ("Cannot read templates path: %s" % (templates_path))
+ print("Cannot read templates path: %s" % (templates_path))
exit(1)
- for template in os.listdir(templates_path):
+ templates = (f for f in os.listdir(templates_path) if fnmatch.fnmatch(f, "*.in"))
+ for template in templates:
template_name = template.split(".", 1)[0]
- with open(os.path.join(templates_path, template), "rb") as fh:
+ with open(os.path.join(templates_path, template), "r") as fh:
TEMPLATES[template_name] = fh.read().replace("\\\n", "")
self.header = ""
self.impl = ""
self.function = ""
- self.function_update = ""
self.class_name = ""
self.description = ""
self.attributes = {}
self.examples = []
+ self.aliases = []
+ self.fuzz_paths = []
+ self.has_options = False
+ self.has_column_aliases = False
+ self.strongly_typed_rows = False
+ self.generator = False
def columns(self):
return [i for i in self.schema if isinstance(i, Column)]
def generate(self, path, template="default"):
"""Generate the virtual table files"""
logging.debug("TableState.generate")
- self.impl_content = jinja2.Template(TEMPLATES[template]).render(
- table_name=self.table_name,
- table_name_cc=to_camel_case(self.table_name),
- schema=self.columns(),
- header=self.header,
- impl=self.impl,
- function=self.function,
- function_update=self.function_update,
- class_name=self.class_name,
- attributes=self.attributes,
- examples=self.examples,
- )
+ all_options = []
+ # Create a list of column options from the kwargs passed to the column.
+ for column in self.columns():
+ column_options = []
+ for option in column.options:
+ # Only allow explicitly-defined options.
+ if option in COLUMN_OPTIONS:
+ column_options.append("ColumnOptions::" + COLUMN_OPTIONS[option])
+ all_options.append(COLUMN_OPTIONS[option])
+ else:
+ print(yellow(
+ "Table %s column %s contains an unknown option: %s" % (
+ self.table_name, column.name, option)))
+ column.options_set = " | ".join(column_options)
+ if len(column.aliases) > 0:
+ self.has_column_aliases = True
+ if len(all_options) > 0:
+ self.has_options = True
+ if "event_subscriber" in self.attributes:
+ self.generator = True
+ if "strongly_typed_rows" in self.attributes:
+ self.strongly_typed_rows = True
+ if "cacheable" in self.attributes:
+ if self.generator:
+ print(lightred(
+ "Table cannot use a generator and be marked cacheable: %s" % (path)))
+ exit(1)
if self.table_name == "" or self.function == "":
- print (lightred("Invalid table spec: %s" % (path)))
+ print(lightred("Invalid table spec: %s" % (path)))
exit(1)
# Check for reserved column names
for column in self.columns():
if column.name in RESERVED:
- print (lightred(("Cannot use column name: %s in table: %s "
- "(the column name is reserved)" % (
- column.name, self.table_name))))
+ print(lightred(("Cannot use column name: %s in table: %s "
+ "(the column name is reserved)" % (
+ column.name, self.table_name))))
+ exit(1)
+
+ if "ADDITIONAL" in all_options and "INDEX" not in all_options:
+ if "no_pkey" not in self.attributes:
+ print(lightred(
+ "Table cannot have 'additional' columns without an index: %s" %(
+ path)))
exit(1)
path_bits = path.split("/")
# May encounter a race when using a make jobserver.
pass
logging.debug("generating %s" % path)
+ self.impl_content = jinja2.Template(TEMPLATES[template]).render(
+ table_name=self.table_name,
+ table_name_cc=to_camel_case(self.table_name),
+ table_name_ucc=to_upper_camel_case(self.table_name),
+ schema=self.columns(),
+ header=self.header,
+ impl=self.impl,
+ function=self.function,
+ class_name=self.class_name,
+ attributes=self.attributes,
+ examples=self.examples,
+ aliases=self.aliases,
+ has_options=self.has_options,
+ has_column_aliases=self.has_column_aliases,
+ generator=self.generator,
+ strongly_typed_rows=self.strongly_typed_rows,
+ attribute_set=[TABLE_ATTRIBUTES[attr] for attr in self.attributes if attr in TABLE_ATTRIBUTES],
+ )
+
with open(path, "w+") as file_h:
file_h.write(self.impl_content)
def blacklist(self, path):
- print (lightred("Blacklisting generated %s" % path))
+ print(lightred("Blacklisting generated %s" % path))
logging.debug("blacklisting %s" % path)
self.generate(path, template="blacklist")
documentation generation and reference.
"""
- def __init__(self, name, col_type, description="", **kwargs):
+ def __init__(self, name, col_type, description="", aliases=[], **kwargs):
self.name = name
self.type = col_type
self.description = description
+ self.aliases = aliases
self.options = kwargs
self.table = kwargs.get("table", "")
-def table_name(name):
+def table_name(name, aliases=[]):
"""define the virtual table name"""
logging.debug("- table_name")
logging.debug(" - called with: %s" % name)
table.description = ""
table.attributes = {}
table.examples = []
+ table.aliases = aliases
def schema(schema_list):
table.schema = schema_list
+def extended_schema(check, schema_list):
+ """
+ define a comparator and a list of Columns objects.
+ """
+ logging.debug("- extended schema")
+ for it in schema_list:
+ if isinstance(it, Column):
+ logging.debug(" - column: %s (%s)" % (it.name, it.type))
+ if not check():
+ it.options['hidden'] = True
+ table.schema.append(it)
+
+
def description(text):
+ if text[-1:] != '.':
+ print(lightred("Table description must end with a period!"))
+ exit(1)
table.description = text
+
def select_all(name=None):
- if name == None:
+ if name is None:
name = table.table_name
return "select count(*) from %s;" % (name)
table.attributes[attr] = kwargs[attr]
-def implementation(impl_string):
+def fuzz_paths(paths):
+ table.fuzz_paths = paths
+
+
+def implementation(impl_string, generator=False):
"""
define the path to the implementation file and the function which
implements the virtual table. You should use the following format:
table.impl = impl
table.function = function
table.class_name = class_name
+ table.generator = generator
'''Check if the table has a subscriber attribute, if so, enforce time.'''
if "event_subscriber" in table.attributes:
+ if not table.table_name.endswith("_events"):
+ print(lightred("Event subscriber must use a '_events' suffix"))
+ sys.exit(1)
columns = {}
# There is no dictionary comprehension on all supported platforms.
for column in table.schema:
sys.exit(1)
-def implementation_update(impl_string=None):
- if impl_string is None:
- table.function_update = ""
- else:
- filename, function = impl_string.split("@")
- class_parts = function.split("::")[::-1]
- table.function_update = class_parts[0]
-
-
-def main(argc, argv):
- parser = argparse.ArgumentParser("Generate C++ Table Plugin from specfile.")
+def main():
+ parser = argparse.ArgumentParser(
+ "Generate C++ Table Plugin from specfile.")
parser.add_argument(
"--debug", default=False, action="store_true",
help="Output debug messages (when developing)"
)
+ parser.add_argument("--disable-blacklist", default=False,
+ action="store_true")
+ parser.add_argument("--header", default=False, action="store_true",
+ help="Generate the header file instead of cpp")
+ parser.add_argument("--foreign", default=False, action="store_true",
+ help="Generate a foreign table")
parser.add_argument("--templates", default=SCRIPT_DIR + "/templates",
- help="Path to codegen output .cpp.in templates")
+ help="Path to codegen output .cpp.in templates")
parser.add_argument("spec_file", help="Path to input .table spec file")
parser.add_argument("output", help="Path to output .cpp file")
args = parser.parse_args()
else:
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
- if argc < 3:
- usage()
- sys.exit(1)
-
filename = args.spec_file
output = args.output
-
if filename.endswith(".table"):
# Adding a 3rd parameter will enable the blacklist
- disable_blacklist = argc > 3
setup_templates(args.templates)
- with open(filename, "rU") as file_handle:
+ with open(filename, "r") as file_handle:
tree = ast.parse(file_handle.read())
exec(compile(tree, "<string>", "exec"))
blacklisted = is_blacklisted(table.table_name, path=filename)
- if not disable_blacklist and blacklisted:
+ if not args.disable_blacklist and blacklisted:
table.blacklist(output)
else:
- table.generate(output)
+ if args.header:
+ template_type = "typed_row"
+ elif args.foreign:
+ template_type = "foreign"
+ else:
+ template_type = "default"
+ table.generate(output, template=template_type)
if __name__ == "__main__":
- SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
- main(len(sys.argv), sys.argv)
+ main()
-/*
- * Copyright (c) 2014, Facebook, Inc.
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
*/
/*
#include <osquery/tables.h>
#include <osquery/registry_factory.h>
-
namespace osquery {
+{% if foreign %}
+void registerForeignTables() {
+{% endif %}
{% for table in tables %}
{{table}}
{% endfor %}
+{% if foreign %}
+}
+{% endif %}
}
-/*
- * Copyright (c) 2014, Facebook, Inc.
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
*/
/*
-/*
- * Copyright (c) 2014, Facebook, Inc.
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
- * This source code is licensed under the BSD-style license found in the
- * LICENSE file in the root directory of this source tree. An additional grant
- * of patent rights can be found in the PATENTS file in the same directory.
- *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
*/
/*
*/
#include <osquery/events.h>
+#include <osquery/logger.h>
#include <osquery/tables.h>
-#include <osquery/status.h>
#include <osquery/registry_factory.h>
namespace osquery {
/// BEGIN[GENTABLE]
namespace tables {
{% if class_name == "" %}\
-osquery::QueryData {{function}}(QueryContext& request);
-{% if function_update != "" %}\
-osquery::Status {{function_update}}(Row& row);
+{% if generator %}\
+void {{function}}(RowYield& yield, QueryContext& context);
+{% elif strongly_typed_rows %}\
+osquery::TableRows {{function}}(QueryContext& context);
+{% else %}\
+osquery::QueryData {{function}}(QueryContext& context);
{% endif %}\
{% else %}
class {{class_name}} {
public:
- osquery::QueryData {{function}}(QueryContext& request);
-{% if function_update != "" %}\
- osquery::Status {{function_update}}(Row& row);
-{% endif %}\
+ void {{function}}(RowYield& yield, QueryContext& context);
};
{% endif %}\
}
class {{table_name_cc}}TablePlugin : public TablePlugin {
private:
- TableColumns columns() const {
+ TableColumns columns() const override {
return {
{% for column in schema %}\
- std::make_tuple("{{column.name}}", {{column.type.affinity}}_TYPE,\
- ColumnOptions::DEFAULT)\
-{% if not loop.last %}, {% endif %}
+ std::make_tuple("{{column.name}}", {{column.type.affinity}},\
+{% if column.options|length > 0 %} {{column.options_set}}\
+{% else %} ColumnOptions::DEFAULT\
+{% endif %}\
+),
{% endfor %}\
};
}
+{% if aliases|length > 0 %}\
- TableRows generate(QueryContext& context) override {
+ std::vector<std::string> aliases() const override {
+ return {
+{% for alias in aliases %}\
+ "{{alias}}",
+{% endfor %}\
+ };
+ }
+{% endif %}\
+
+{% if has_column_aliases %}\
+
+ ColumnAliasSet columnAliases() const override {
+ return {
+{% for column in schema %}\
+{% if column.aliases|length > 0 %}\
+ {"{{column.name}}", {% raw %}{{% endraw %}\
+{% for alias in column.aliases %}"{{alias}}"\
+{% if not loop.last %}, {% endif %}\
+{% endfor %}}},
+{% endif %}\
+{% endfor %}\
+ };
+ }
+
+ AliasColumnMap aliasedColumns() const override {
+ return {
+{% for column in schema %}\
+{% if column.aliases|length > 0 %}\
+{% for alias in column.aliases %}\
+ { "{{alias}}", "{{column.name}}" },
+{% endfor %}\
+{% endif %}\
+{% endfor %}\
+ };
+ }
+{% endif %}\
+
+ TableAttributes attributes() const override {
+ return \
+{% for attribute in attribute_set %}\
+ TableAttributes::{{attribute}} |\
+{% endfor %}\
+ TableAttributes::NONE;
+ }
+
+{% if generator %}\
+ bool usesGenerator() const override { return true; }
+
+ void generator(RowYield& yield, QueryContext& context) override {
{% if class_name != "" %}\
- if (EventFactory::exists("{{class_name}}")) {
- auto subscriber = EventFactory::getEventSubscriber("{{class_name}}");
- return subscriber->{{function}}(request);
+ if (EventFactory::exists(getName())) {
+ auto subscriber = EventFactory::getEventSubscriber(getName());
+ return subscriber->{{function}}(yield, context);
} else {
- return {};
+ LOG(ERROR) << "Subscriber table missing: " << getName();
}
{% else %}\
- TableRows results = osquery::tableRowsFromQueryData(tables::{{function}}(context));
+ tables::{{function}}(yield, context);
{% endif %}\
- return results;
}
-
-{% if function_update != "" %}\
- Status update(Row& row) {
- return tables::{{function_update}}(row);
+{% else %}\
+ TableRows generate(QueryContext& context) override {
+{% if attributes.cacheable %}\
+ if (isCached(kCacheStep, context)) {
+ return getCache();
+ }
+{% endif %}\
+{% if attributes.strongly_typed_rows %}\
+ TableRows results = tables::{{function}}(context);
+{% else %}\
+ TableRows results = osquery::tableRowsFromQueryData(tables::{{function}}(context));
+{% endif %}
+{% if attributes.cacheable %}\
+ setCache(kCacheStep, kCacheInterval, context, results);
+{% endif %}
+ return results;
}
{% endif %}\
-};
+};
{% if attributes.utility %}
REGISTER_INTERNAL({{table_name_cc}}TablePlugin, "table", "{{table_name}}");
--- /dev/null
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
+ */
+
+/*
+** This file is generated. Do not modify it manually!
+*/
+
+#include <osquery/events.h>
+#include <osquery/logger.h>
+#include <osquery/tables.h>
+#include <osquery/registry_factory.h>
+
+namespace osquery {
+namespace tables {
+
+auto {{table_name_cc}}Register = []() {
+/// BEGIN[GENTABLE]
+ class {{table_name_cc}}TablePlugin : public TablePlugin {
+ private:
+ TableColumns columns() const override {
+ return {
+{% for column in schema %}\
+ std::make_tuple("{{column.name}}", {{column.type.affinity}},\
+{% if column.options|length > 0 %} {{column.options_set}}\
+{% else %} ColumnOptions::DEFAULT\
+{% endif %}\
+),
+{% endfor %}\
+ };
+ }
+{% if aliases|length > 0 %}\
+
+ std::vector<std::string> aliases() const override {
+ return {
+{% for alias in aliases %}\
+ "{{alias}}",
+{% endfor %}\
+ };
+ }
+{% endif %}\
+
+{% if has_column_aliases %}\
+
+ ColumnAliasSet columnAliases() const override {
+ return {
+{% for column in schema %}\
+{% if column.aliases|length > 0 %}\
+ {"{{column.name}}", {% raw %}{{% endraw %}\
+{% for alias in column.aliases %}"{{alias}}"\
+{% if not loop.last %}, {% endif %}\
+{% endfor %}}},
+{% endif %}\
+{% endfor %}\
+ };
+ }
+{% endif %}\
+
+ TableRows generate(QueryContext& request) override { return TableRows(); }
+ };
+
+ {
+ auto registry = RegistryFactory::get().registry("table");
+ registry->add("{{table_name}}",
+ std::make_shared<{{table_name_cc}}TablePlugin>(), false);
+ }
+/// END[GENTABLE]
+};
+
+}
+}
--- /dev/null
+/**
+ * Copyright (c) 2014-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed in accordance with the terms specified in
+ * the LICENSE file found in the root directory of this source tree.
+ */
+
+/*
+** This file is generated. Do not modify it manually!
+*/
+
+#include <osquery/tables.h>
+
+namespace osquery {
+namespace tables {
+
+class {{table_name_ucc}}Row : public TableRow {
+public:
+ {{table_name_ucc}}Row() {
+ }
+
+{% for column in schema %}\
+ {{column.type.type}} {{column.name}}_col;
+{% endfor %}\
+
+ enum Column {
+{% for column in schema %}\
+{% if loop.index0 < 63 %}\
+ {{column.name.upper()}} = 1ULL << {{loop.index0}},
+{% else %}\
+ {{column.name.upper()}} = 1ULL << 63,
+{% endif %}\
+{% endfor %}\
+ };
+
+ virtual int get_rowid(sqlite_int64 default_value, sqlite_int64* pRowid) const override {
+{% set filtered = schema|select("equalto", "rowid") %}\
+{% if filtered|list|length == 1 %}\
+ *pRowid = rowid_col;
+{% else %}\
+ *pRowid = default_value;
+{% endif %}\
+ return SQLITE_OK;
+ }
+
+ virtual int get_column(sqlite3_context* ctx, sqlite3_vtab* vtab, int col) override {
+ switch (col) {
+{% for column in schema %}\
+ case {{loop.index0}}:
+{% if column.type.affinity == "TEXT_TYPE" %}\
+ sqlite3_result_text(ctx, {{column.name}}_col.c_str(), static_cast<int>({{column.name}}_col.size()), SQLITE_STATIC);
+{% elif column.type.affinity == "INTEGER_TYPE" %}\
+ sqlite3_result_int(ctx, {{column.name}}_col);
+{% elif column.type.affinity == "BIGINT_TYPE" or column.type.affinity == "UNSIGNED_BIGINT_TYPE" %}\
+ sqlite3_result_int64(ctx, {{column.name}}_col);
+{% elif column.type.affinity == "DOUBLE_TYPE" %}\
+ sqlite3_result_double(ctx, {{column.name}}_col);
+{% endif %}\
+ break;
+{% endfor %}\
+ }
+ return SQLITE_OK;
+ }
+
+ virtual Status serialize(JSON& doc, rapidjson::Value& obj) const override {
+{% for column in schema %}\
+{% if column.type.affinity == "TEXT_TYPE" %}\
+ doc.addRef("{{column.name}}", {{column.name}}_col);
+{% else %}\
+ doc.add("{{column.name}}", {{column.name}}_col);
+{% endif %}\
+{% endfor %}\
+
+ return Status();
+ }
+
+ virtual operator Row() const override {
+ Row result;
+
+{% for column in schema %}\
+{% if column.type.affinity == "TEXT_TYPE" %}\
+ result["{{column.name}}"] = {{column.name}}_col;
+{% elif column.type.affinity == "INTEGER_TYPE" %}\
+ result["{{column.name}}"] = INTEGER({{column.name}}_col);
+{% elif column.type.affinity == "BIGINT_TYPE" %}\
+ result["{{column.name}}"] = BIGINT({{column.name}}_col);
+{% elif column.type.affinity == "UNSIGNED_BIGINT_TYPE" %}\
+ result["{{column.name}}"] = UNSIGNED_BIGINT({{column.name}}_col);
+{% elif column.type.affinity == "DOUBLE_TYPE" %}\
+ result["{{column.name}}"] = DOUBLE({{column.name}}_col);
+{% endif %}\
+{% endfor %}\
+
+ return result;
+ }
+
+ virtual TableRowHolder clone() const override {
+ return TableRowHolder(new {{table_name_ucc}}Row(*this));
+ }
+};
+}
+}