Skip to content

Commit

Permalink
sudoers table: Support file and directory includes (osquery#5350)
Browse files Browse the repository at this point in the history
Summary:
This adds support for the `#includedir` and `#include` directives to the `sudoers` table, making `sudoers` behave more like the actual `sudo` rule parser:

* When an `includefile` directive is encountered, the referenced file will be parsed using the same rules as the top-level sudoers file.
* When an `includedir` directive is encountered, the referenced directory will be listed and each valid file within (i.e., each file *not* containing a `.` and *not* ending with `~`) will be parsed using the same rules as the top-level sudoers file.
* An additional `source` column tracks the file that provides the row's rule.
* Like `sudoers(5)`, nesting is limited to 128 individual files, with directory inclusions being counted once for each file they contain.
Pull Request resolved: osquery#5350

Differential Revision: D13717394

Pulled By: akindyakov

fbshipit-source-id: 9659526f21e82c712c495caa80775b15d7e47e37
  • Loading branch information
woodruffw authored and facebook-github-bot committed Jan 18, 2019
1 parent e401a5e commit bab228b
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 19 deletions.
2 changes: 2 additions & 0 deletions osquery/tables/system/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ osquery_cxx_library(
[
"posix/known_hosts.h",
"posix/shell_history.h",
"posix/sudoers.h",
"posix/sysctl_utils.h",
],
),
Expand Down Expand Up @@ -288,6 +289,7 @@ osquery_cxx_library(
osquery_target("osquery/tables/system/tests:users_tests"),
osquery_target("osquery/tables/system/tests:known_hosts_tests"),
osquery_target("osquery/tables/system/tests:shell_history_tests"),
osquery_target("osquery/tables/system/tests:sudoers_tests"),
osquery_target("osquery/tables/system/tests:yum_sources_tests"),
osquery_target("osquery/tables/system/tests:system_tables_tests"),
osquery_target("osquery/tables/system/tests:apps_tests"),
Expand Down
123 changes: 104 additions & 19 deletions osquery/tables/system/posix/sudoers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
#include <vector>

#include <boost/algorithm/string/trim.hpp>
#include <boost/filesystem/path.hpp>

#include <osquery/filesystem/filesystem.h>
#include <osquery/logger.h>
#include <osquery/tables.h>
#include <osquery/utils/conversions/join.h>
#include <osquery/utils/conversions/split.h>

namespace fs = boost::filesystem;

namespace osquery {
namespace tables {

Expand All @@ -27,42 +31,123 @@ const std::string kSudoFile = "/etc/sudoers";
const std::string kSudoFile = "/usr/local/etc/sudoers";
#endif

QueryData genSudoers(QueryContext& context) {
QueryData results;
// sudoers(5): No more than 128 files are allowed to be nested.
static const unsigned int kMaxNest = 128;

if (!isReadable(kSudoFile).ok()) {
return results;
void genSudoersFile(const std::string& filename,
unsigned int level,
QueryData& results) {
if (level > kMaxNest) {
TLOG << "sudoers file recursion maximum reached";
return;
}

std::string contents;
if (!forensicReadFile(kSudoFile, contents).ok()) {
return results;
if (!forensicReadFile(filename, contents).ok()) {
TLOG << "couldn't read sudoers file: " << filename;
return;
}

auto lines = split(contents, "\n");
std::vector<std::string> valid_lines;

for (auto& line : lines) {
Row r;
boost::trim(line);

// Only add lines that are not comments or blank.
if (line.size() > 0 && line.at(0) != '#') {
valid_lines.push_back(line);
r["source"] = filename;

auto header_pos = line.find_first_of("\t\v ");
r["header"] = line.substr(0, header_pos);

if (header_pos == std::string::npos) {
header_pos = line.size() - 1;
}

r["rule_details"] = line.substr(header_pos + 1);

results.push_back(std::move(r));
} else if (line.find("#includedir") == 0) {
auto space = line.find_first_of(' ');

// If #includedir doesn't look like it's followed by
// a path, treat it like a normal comment.
if (space == std::string::npos) {
continue;
}

auto inc_dir = line.substr(space + 1);

// NOTE(ww): See sudo NEWS for 1.8.4:
// Both #include and #includedir support relative paths.
if (inc_dir.at(0) != '/') {
auto path = fs::path(filename).parent_path() / inc_dir;
inc_dir = path.string();
}

// Build and push the row before recursing.
r["source"] = filename;
r["header"] = "#includedir";
r["rule_details"] = inc_dir;
results.push_back(std::move(r));

std::vector<std::string> inc_files;
if (!listFilesInDirectory(inc_dir, inc_files).ok()) {
TLOG << "couldn't list includedir: " << inc_dir;
continue;
}

for (const auto& inc_file : inc_files) {
std::string inc_basename = fs::path(inc_file).filename().string();

// Per sudoers(5): Any files in the included directory that
// contain a '.' or end with '~' are ignored.
if (inc_basename.empty() ||
inc_basename.find('.') != std::string::npos ||
inc_basename.back() == '~') {
continue;
}

genSudoersFile(inc_file, ++level, results);
}
} else if (line.find("#include") == 0) {
auto space = line.find_first_of(' ');

// If #include doesn't look like it's followed by
// a path, treat it like a normal comment.
if (space == std::string::npos) {
continue;
}

auto inc_file = line.substr(space + 1);

// Per sudoers(5): If the included file doesn't
// start with /, read it relative to the current file.
if (inc_file.at(0) != '/') {
const auto path = fs::path(filename).parent_path() / inc_file;
inc_file = path.string();
}

r["source"] = filename;
r["header"] = "#include";
r["rule_details"] = inc_file;
results.push_back(std::move(r));

genSudoersFile(inc_file, ++level, results);
}
}
}

for (const auto& line : valid_lines) {
Row r;
auto cols = split(line);
r["header"] = cols.at(0);

cols.erase(cols.begin());
r["rule_details"] = join(cols, " ");
QueryData genSudoers(QueryContext& context) {
QueryData results;

results.push_back(r);
if (!isReadable(kSudoFile).ok()) {
return results;
}

genSudoersFile(kSudoFile, 1, results);

return results;
}
}
}
} // namespace tables
} // namespace osquery
26 changes: 26 additions & 0 deletions osquery/tables/system/posix/sudoers.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under both the Apache 2.0 license (found in the
* LICENSE file in the root directory of this source tree) and the GPLv2 (found
* in the COPYING file in the root directory of this source tree).
* You may select, at your option, one of the above-listed licenses.
*/

#include <osquery/query.h>
#include <osquery/tables.h>

#include <string>

namespace osquery {
namespace tables {

void genSudoersFile(const std::string& filename,
unsigned int level,
QueryData& results);

QueryData genSudoers(QueryContext& context);

} // namespace tables
} // namespace osquery
27 changes: 27 additions & 0 deletions osquery/tables/system/tests/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,33 @@ osquery_cxx_test(
],
)

osquery_cxx_test(
name = "sudoers_tests",
platform_srcs = [
(
POSIX,
[
"posix/sudoers_tests.cpp",
],
),
],
visibility = ["PUBLIC"],
deps = [
osquery_target("osquery/tables/system:system_table"),
osquery_target("osquery/core:core"),
osquery_target("osquery/filesystem:osquery_filesystem"),
osquery_target("osquery/utils:utils"),
osquery_target("osquery/utils/conversions:conversions"),
osquery_target("osquery/tables/system:system_table"),
osquery_target("osquery/config/tests:test_utils"),
osquery_target("osquery/remote/tests:remote_test_utils"),
osquery_target("osquery/filesystem:osquery_filesystem"),
osquery_target("osquery/database:database"),
osquery_target("osquery/database/plugins:ephemeral"),
osquery_target("osquery/core/sql:core_sql"),
],
)

osquery_cxx_test(
name = "yum_sources_tests",
platform_srcs = [
Expand Down
140 changes: 140 additions & 0 deletions osquery/tables/system/tests/posix/sudoers_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/**
* Copyright (c) 2014-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under both the Apache 2.0 license (found in the
* LICENSE file in the root directory of this source tree) and the GPLv2 (found
* in the COPYING file in the root directory of this source tree).
* You may select, at your option, one of the above-listed licenses.
*/

#include <fstream>

#include <boost/filesystem.hpp>

#include <gtest/gtest.h>

#include <osquery/sql.h>
#include <osquery/tables/system/posix/sudoers.h>
#include <osquery/utils/scope_guard.h>

namespace fs = boost::filesystem;

namespace osquery {
namespace tables {

static fs::path real_temp_path() {
auto temp_dir = fs::temp_directory_path();

// NOTE(ww): The sudoers table expands paths to their canonical
// form when listing directories, so we need to make sure that
// the temp directory is canonicalized as well.
return fs::canonical(temp_dir);
}

class SudoersTests : public testing::Test {};

TEST_F(SudoersTests, basic_sudoers) {
auto directory =
real_temp_path() / fs::unique_path("osquery.sudoers_tests.%%%%-%%%%");

ASSERT_TRUE(fs::create_directories(directory));

auto const path_guard =
scope_guard::create([directory]() { fs::remove_all(directory); });

auto sudoers_file = directory / fs::path("sudoers");

{
auto fout = std::ofstream(sudoers_file.native());
fout << "Defaults env_reset" << '\n';
}

auto results = QueryData{};
genSudoersFile(sudoers_file.string(), 1, results);

ASSERT_EQ(results.size(), 1);

const auto& row = results[0];
ASSERT_EQ(row.at("source"), sudoers_file.string());
ASSERT_EQ(row.at("header"), "Defaults");
ASSERT_EQ(row.at("rule_details"), "env_reset");
}

TEST_F(SudoersTests, include_file) {
auto directory =
real_temp_path() / fs::unique_path("osquery.sudoers_tests.%%%%-%%%%");

ASSERT_TRUE(fs::create_directories(directory));

auto const path_guard =
scope_guard::create([directory]() { fs::remove_all(directory); });

auto sudoers_top = directory / fs::path("sudoers");
auto sudoers_inc = directory / fs::path("sudoers_inc");

{
auto fout_top = std::ofstream(sudoers_top.native());
fout_top << "#include sudoers_inc" << '\n';

auto fout_inc = std::ofstream(sudoers_inc.native());
fout_inc << "Defaults env_reset" << '\n';
}

auto results = QueryData{};
genSudoersFile(sudoers_top.string(), 1, results);

ASSERT_EQ(results.size(), 2);

const auto& first_row = results[0];
ASSERT_EQ(first_row.at("source"), sudoers_top.string());
ASSERT_EQ(first_row.at("header"), "#include");
ASSERT_EQ(first_row.at("rule_details"), sudoers_inc.string());

const auto& second_row = results[1];
ASSERT_EQ(second_row.at("source"), sudoers_inc.string());
ASSERT_EQ(second_row.at("header"), "Defaults");
ASSERT_EQ(second_row.at("rule_details"), "env_reset");
}

TEST_F(SudoersTests, include_dir) {
auto directory =
real_temp_path() / fs::unique_path("osquery.sudoers_tests.%%%%-%%%%");

ASSERT_TRUE(fs::create_directories(directory));

auto const path_guard =
scope_guard::create([directory]() { fs::remove_all(directory); });

auto sudoers_top = directory / fs::path("sudoers");
auto sudoers_dir = directory / fs::path("sudoers.d");
auto sudoers_inc = sudoers_dir / fs::path("sudoers_inc");

ASSERT_TRUE(fs::create_directories(sudoers_dir));

{
auto fout_top = std::ofstream(sudoers_top.native());
fout_top << "#includedir " << sudoers_dir.string() << '\n';

auto fout_inc = std::ofstream(sudoers_inc.native());
fout_inc << "Defaults env_reset" << '\n';
}

auto results = QueryData{};
genSudoersFile(sudoers_top.string(), 1, results);

ASSERT_EQ(results.size(), 2);

const auto& first_row = results[0];
ASSERT_EQ(first_row.at("source"), sudoers_top.string());
ASSERT_EQ(first_row.at("header"), "#includedir");
ASSERT_EQ(first_row.at("rule_details"), sudoers_dir.string());

const auto& second_row = results[1];
ASSERT_EQ(second_row.at("source"), sudoers_inc.string());
ASSERT_EQ(second_row.at("header"), "Defaults");
ASSERT_EQ(second_row.at("rule_details"), "env_reset");
}

} // namespace tables
} // namespace osquery
1 change: 1 addition & 0 deletions specs/posix/sudoers.table
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
table_name("sudoers")
description("Rules for running commands as other users via sudo.")
schema([
Column("source", TEXT, "Source file containing the given rule"),
Column("header", TEXT, "Symbol for given rule"),
Column("rule_details", TEXT, "Rule definition")
])
Expand Down

0 comments on commit bab228b

Please sign in to comment.