Bump codegen to upstream-4.0.0
authorSangwan Kwon <sangwan.kwon@samsung.com>
Mon, 16 Sep 2019 02:40:48 +0000 (11:40 +0900)
committer권상완/Security 2Lab(SR)/Engineer/삼성전자 <sangwan.kwon@samsung.com>
Mon, 16 Sep 2019 06:08:51 +0000 (15:08 +0900)
Signed-off-by: Sangwan Kwon <sangwan.kwon@samsung.com>
18 files changed:
specs/CMakeLists.txt
specs/groups.table
specs/processes.table
specs/users.table
specs/utility/file.table [new file with mode: 0644]
specs/utility/time.table [new file with mode: 0644]
src/osquery/tables/system/linux/groups.cpp
src/osquery/tables/system/linux/processes.cpp
src/osquery/tables/system/linux/users.cpp
src/osquery/tables/utility/file.cpp [new file with mode: 0644]
src/osquery/tables/utility/time.cpp [new file with mode: 0644]
tools/codegen/amalgamate.py
tools/codegen/gentable.py
tools/codegen/templates/amalgamation.cpp.in
tools/codegen/templates/blacklist.cpp.in
tools/codegen/templates/default.cpp.in
tools/codegen/templates/foreign.cpp.in [new file with mode: 0644]
tools/codegen/templates/typed_row.h.in [new file with mode: 0644]

index 7f75647..a7b5310 100644 (file)
@@ -60,7 +60,10 @@ SET(AMALGAMATION_FILE_GEN "${OSQUERY_GENERATED_PATH}/amalgamation.cpp")
 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}")
index 2a16be3..a670907 100644 (file)
@@ -1,13 +1,23 @@
-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
index 078dae0..e07c6ac 100644 (file)
@@ -1,25 +1,48 @@
 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",
index e5dab55..9b01765 100644 (file)
@@ -1,17 +1,24 @@
 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'",
diff --git a/specs/utility/file.table b/specs/utility/file.table
new file mode 100644 (file)
index 0000000..43047f3
--- /dev/null
@@ -0,0 +1,34 @@
+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/%'",
+])
diff --git a/specs/utility/time.table b/specs/utility/time.table
new file mode 100644 (file)
index 0000000..6177722
--- /dev/null
@@ -0,0 +1,26 @@
+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")
index b537db9..a2a0f51 100644 (file)
@@ -1,46 +1,58 @@
-/*
- *  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;
 }
index 888f728..6ab8334 100644 (file)
@@ -1,30 +1,42 @@
-/*
- *  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;
 }
 
@@ -43,20 +55,71 @@ inline std::string readProcCMDLine(const std::string& pid) {
   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 &&
@@ -101,23 +164,27 @@ void genProcessMap(const std::string& pid, QueryData& results) {
   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];
 
@@ -128,127 +195,300 @@ void genProcessMap(const std::string& pid, QueryData& results) {
     }
 
     // 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;
@@ -275,5 +515,16 @@ QueryData genProcessMemoryMap(QueryContext& context) {
 
   return results;
 }
+
+QueryData genProcessNamespaces(QueryContext& context) {
+  QueryData results;
+
+  const auto pidlist = getProcList(context);
+  for (const auto& pid : pidlist) {
+    genNamespaces(pid, results);
+  }
+
+  return results;
+}
 }
 }
index 6108987..d951e2a 100644 (file)
@@ -1,64 +1,86 @@
-/*
- *  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");
-}
 }
 }
diff --git a/src/osquery/tables/utility/file.cpp b/src/osquery/tables/utility/file.cpp
new file mode 100644 (file)
index 0000000..c72ab73
--- /dev/null
@@ -0,0 +1,199 @@
+/**
+ *  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
diff --git a/src/osquery/tables/utility/time.cpp b/src/osquery/tables/utility/time.cpp
new file mode 100644 (file)
index 0000000..8052d34
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ *  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
index 3d47df0..a785770 100755 (executable)
@@ -1,33 +1,27 @@
 #!/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()
@@ -46,29 +40,41 @@ def genTableData(filename):
 
 
 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
 
index 163f37a..e9f9995 100755 (executable)
@@ -1,11 +1,10 @@
 #!/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
@@ -14,13 +13,13 @@ from __future__ import unicode_literals
 
 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"
@@ -31,11 +30,11 @@ TEMPLATES = {}
 # 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
@@ -45,13 +44,14 @@ class DataType(object):
         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"
@@ -61,10 +61,49 @@ NETWORK = "NETWORK"
 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):
@@ -72,6 +111,10 @@ 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)
@@ -82,7 +125,7 @@ def is_blacklisted(table_name, path=None, blacklist=None):
     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
@@ -95,20 +138,32 @@ def is_blacklisted(table_name, path=None, blacklist=None):
         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", "")
 
 
@@ -141,11 +196,16 @@ class TableState(Singleton):
         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)]
@@ -156,29 +216,51 @@ class TableState(Singleton):
     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("/")
@@ -193,11 +275,30 @@ class TableState(Singleton):
                     # 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")
 
@@ -212,10 +313,11 @@ class Column(object):
     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
 
 
@@ -231,7 +333,7 @@ class ForeignKey(object):
         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)
@@ -239,6 +341,7 @@ def table_name(name):
     table.description = ""
     table.attributes = {}
     table.examples = []
+    table.aliases = aliases
 
 
 def schema(schema_list):
@@ -255,11 +358,28 @@ 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)
 
@@ -273,7 +393,11 @@ def attributes(**kwargs):
         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:
@@ -294,9 +418,13 @@ def implementation(impl_string):
     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:
@@ -313,23 +441,21 @@ def implementation(impl_string):
             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()
@@ -339,27 +465,26 @@ def main(argc, argv):
     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()
index 65cdb46..a0d2633 100644 (file)
@@ -1,11 +1,9 @@
-/*
- *  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 %}
 }
index 3a5a40b..3a27c60 100644 (file)
@@ -1,11 +1,9 @@
-/*
- *  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.
  */
 
 /*
index dc4051f..1d3c7d6 100644 (file)
@@ -1,11 +1,9 @@
-/*
- *  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.
  */
 
 /*
@@ -13,8 +11,8 @@
 */
 
 #include <osquery/events.h>
+#include <osquery/logger.h>
 #include <osquery/tables.h>
-#include <osquery/status.h>
 #include <osquery/registry_factory.h>
 
 namespace osquery {
@@ -22,54 +20,116 @@ 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}}");
diff --git a/tools/codegen/templates/foreign.cpp.in b/tools/codegen/templates/foreign.cpp.in
new file mode 100644 (file)
index 0000000..a9c4748
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ *  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]
+};
+
+}
+}
diff --git a/tools/codegen/templates/typed_row.h.in b/tools/codegen/templates/typed_row.h.in
new file mode 100644 (file)
index 0000000..991b0b3
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ *  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));
+  }
+};
+}
+}