#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
#include "argument_retriever.hpp"
#include "clock.hpp"
#include "condition/base.hpp"
#include "condition/cmdi_detector.hpp"
#include "condition/shi_common.hpp"
#include "condition/structured_condition.hpp"
#include "cow_string.hpp"
#include "exception.hpp"
#include "exclusion/common.hpp"
#include "iterator.hpp"
#include "log.hpp"
#include "object.hpp"
#include "object_type.hpp"
#include "platform.hpp"
#include "tokenizer/shell.hpp"
#include "transformer/lowercase.hpp"
#include "utils.hpp"
using namespace std::literals;
namespace ddwaf {
namespace {
class scalar_iterator {
public:
explicit scalar_iterator(object_view obj, const std::span<const std::string> & ,
const object_set_ref & )
: current_(obj)
{}
~scalar_iterator() = default;
scalar_iterator(const scalar_iterator &) = default;
scalar_iterator(scalar_iterator &&) noexcept = default;
scalar_iterator &operator=(const scalar_iterator &) = delete;
scalar_iterator &operator=(scalar_iterator &&) noexcept = delete;
[[nodiscard]] object_view operator*() { return current_; }
bool operator++()
{
current_ = nullptr;
return false;
}
[[nodiscard]] explicit operator bool() const { return current_.has_value(); }
[[nodiscard]] static std::vector<std::variant<std::string_view, int64_t>> get_current_path()
{
return {};
}
protected:
object_view current_;
};
struct opt_spec {
bool requires_command_opt{false};
bool command_after_opt{false};
platform shell_platform;
std::unordered_set<std::string_view> command_opt;
std::unordered_set<std::string_view> opts_with_arg;
};
std::unordered_map<std::string_view, opt_spec> known_shells{
{"sh", {.requires_command_opt = true,
.command_after_opt = false,
.shell_platform = platform::linux,
.command_opt = {"c"},
.opts_with_arg = {"O", "o", "init-file", "rcfile"}}},
{"bash", {.requires_command_opt = true,
.command_after_opt = false,
.shell_platform = platform::linux,
.command_opt = {"c"},
.opts_with_arg = {"O", "o", "init-file", "rcfile"}}},
{"ksh", {.requires_command_opt = false,
.command_after_opt = false,
.shell_platform = platform::linux,
.command_opt = {"c"},
.opts_with_arg = {"o", "T"}}},
{"rksh", {.requires_command_opt = false,
.command_after_opt = false,
.shell_platform = platform::linux,
.command_opt = {"c"},
.opts_with_arg = {"o", "T"}}},
{"fish", {.requires_command_opt = true,
.command_after_opt = true,
.shell_platform = platform::linux,
.command_opt = {"c", "command"},
.opts_with_arg = {}}},
{"zsh", {.requires_command_opt = true,
.command_after_opt = false,
.shell_platform = platform::linux,
.command_opt = {"c"},
.opts_with_arg = {"o"}}},
{"dash", {.requires_command_opt = true,
.command_after_opt = false,
.shell_platform = platform::linux,
.command_opt = {"c"},
.opts_with_arg = {"o"}}},
{"ash", {.requires_command_opt = true,
.command_after_opt = false,
.shell_platform = platform::linux,
.command_opt = {"c"},
.opts_with_arg = {"o"}}},
{"powershell", {.requires_command_opt = true,
.command_after_opt = true,
.shell_platform = platform::windows,
.command_opt = {"command", "commandwithargs"},
.opts_with_arg = {}}},
{"pwsh", {.requires_command_opt = true,
.command_after_opt = true,
.shell_platform = platform::windows,
.command_opt = {"command", "commandwithargs"},
.opts_with_arg = {}}},
};
std::string_view basename(std::string_view path)
{
std::size_t idx = std::string_view::npos;
if (system_platform::is(platform::windows)) {
idx = path.find_last_of(R"(\/)"sv);
} else {
idx = path.find_last_of('/');
}
return idx == std::string_view::npos ? path : path.substr(idx + 1);
}
std::string_view trim_quotes(std::string_view str)
{
if (str.size() > 1 && ((str.front() == '"' && str.back() == '"') ||
(str.front() == '\'' && str.back() == '\''))) {
str.remove_prefix(1);
str.remove_suffix(1);
}
return str;
}
std::string_view trim_whitespaces(std::string_view str)
{
static const std::string_view whitespaces = " \f\n\r\t\v";
if (str.empty()) {
return {};
}
auto start = str.find_first_not_of(whitespaces);
if (start == std::string_view::npos) {
return {};
}
auto end = str.find_last_not_of(whitespaces);
return str.substr(start, 1 + end - start);
}
enum class opt_type : uint8_t { none, short_opt, long_opt, end_opt };
inline std::pair<std::string_view, std::string_view> split_long_opt_with_arg(std::string_view opt)
{
if (opt.size() < 3) {
return {opt, {}};
}
auto idx = opt.find('=');
if (idx == std::string_view::npos || idx == 0 || idx == opt.size() - 1) {
return {opt, {}};
}
return {opt.substr(0, idx), trim_quotes(opt.substr(idx + 1, opt.size() - (idx + 1)))};
}
inline std::tuple<std::string_view, std::string_view, opt_type> parse_option(
const opt_spec &spec, std::string_view arg)
{
if (spec.shell_platform == platform::windows && (arg[0] == '-' || arg[0] == '/')) {
arg.remove_prefix(1);
if (arg.starts_with('-')) {
arg.remove_prefix(1);
}
return {arg, {}, opt_type::long_opt};
}
if (spec.shell_platform == platform::linux) {
if (arg[0] == '-') {
arg.remove_prefix(1);
if (arg.starts_with('-')) {
arg.remove_prefix(1);
if (arg.empty()) {
return {{}, {}, opt_type::end_opt};
}
auto [key, value] = split_long_opt_with_arg(arg);
return {key, value, opt_type::long_opt};
}
return {arg, {}, opt_type::short_opt};
}
if (arg[0] == '+') {
arg.remove_prefix(1);
return {arg, {}, opt_type::short_opt};
}
}
return {{}, {}, opt_type::none};
}
std::string_view find_shell_command(std::string_view executable, object_view exec_args)
{
cow_string executable_lc{executable};
if (system_platform::is(platform::windows)) {
if (transformer::lowercase::transform(executable_lc)) {
executable = static_cast<std::string_view>(executable_lc);
}
if (executable.ends_with(".exe")) {
executable.remove_suffix(4);
}
}
auto shell_it = known_shells.find(basename(executable));
if (shell_it == known_shells.end()) {
return {};
}
auto &spec = shell_it->second;
bool command_opt_found = !spec.requires_command_opt;
for (std::size_t i = 1; i < exec_args.size(); ++i) {
auto arg = exec_args.at_value(i).as_or_default<std::string_view>({});
if (arg.empty()) {
continue;
}
auto [opt, embedded_arg, type] = parse_option(spec, arg);
if (type == opt_type::short_opt) {
for (std::size_t j = 0; j < opt.size(); ++j) {
const auto single_opt = opt.substr(j, 1);
command_opt_found = command_opt_found || spec.command_opt.contains(single_opt);
if (spec.command_after_opt && command_opt_found) {
if (i + 1 >= exec_args.size()) {
return {};
}
return exec_args.at_value(i + 1).as_or_default<std::string_view>({});
}
if (spec.opts_with_arg.contains(single_opt)) {
++i;
}
}
} else if (type == opt_type::long_opt) {
cow_string opt_lc{opt};
if (spec.shell_platform == platform::windows &&
transformer::lowercase::transform(opt_lc)) {
opt = static_cast<std::string_view>(opt_lc);
}
command_opt_found = command_opt_found || spec.command_opt.contains(opt);
if (spec.command_after_opt && command_opt_found) {
if (embedded_arg.empty() && i + 1 >= exec_args.size()) {
break;
}
return embedded_arg.empty()
? exec_args.at_value(i + 1).as_or_default<std::string_view>({})
: embedded_arg;
}
if (spec.opts_with_arg.contains(opt)) {
++i;
}
} else if (type == opt_type::end_opt) {
if (!command_opt_found || i + 1 >= exec_args.size()) {
break;
}
return exec_args.at_value(i + 1).as_or_default<std::string_view>({});
} else {
if (!command_opt_found) {
break;
}
return arg;
}
}
return {};
}
std::optional<shi_result> cmdi_impl(object_view exec_args,
std::vector<shell_token> &resource_tokens, object_view params,
const object_set_ref &objects_excluded, ddwaf::timer &deadline)
{
const std::string_view executable =
trim_whitespaces(exec_args.at_value(0).as_or_default<std::string_view>({}));
if (executable.empty()) {
return {};
}
const auto exec_basename = basename(executable);
const bool eval_executable = (exec_basename != executable);
auto shell_command = find_shell_command(exec_basename, exec_args);
if (shell_command.empty() && !eval_executable) {
return std::nullopt;
}
kv_iterator it(params, {}, objects_excluded);
for (; it; ++it) {
if (deadline.expired()) {
throw ddwaf::timeout_exception();
}
const object_view param = *it;
if (!param.is_string()) {
continue;
}
if (eval_executable) {
auto value = param.as<std::string_view>();
value = trim_whitespaces(value);
if (executable == value) {
return {{.value = std::string(value), .key_path = it.get_current_path()}};
}
}
if (!shell_command.empty()) {
auto res = find_shi_from_params<std::string_view, scalar_iterator>(
shell_command, resource_tokens, param, objects_excluded, deadline);
if (res.has_value()) {
res->key_path = it.get_current_path();
return res;
}
}
}
return std::nullopt;
}
std::string generate_string_resource(object_view root)
{
std::string resource;
for (std::size_t i = 0; i < root.size(); ++i) {
auto child = root.at_value(i).as_or_default<std::string_view>({});
if (i > 0) {
resource.append(R"( ")");
resource.append(child);
resource.append(R"(")");
} else {
resource.append(child);
}
}
return resource;
}
}
cmdi_detector::cmdi_detector(std::vector<condition_parameter> args)
: base_impl<cmdi_detector>(std::move(args))
{}
bool cmdi_detector::eval_impl(const unary_argument<object_view> &resource,
const variadic_argument<object_view> ¶ms, condition_cache &cache,
const object_set_ref &objects_excluded, ddwaf::timer &deadline) const
{
if (resource.value.type() != object_type::array || resource.value.empty()) {
return {};
}
std::vector<shell_token> resource_tokens;
for (const auto ¶m : params) {
auto res =
cmdi_impl(resource.value, resource_tokens, param.value, objects_excluded, deadline);
if (res.has_value()) {
auto &[highlight, param_kp] = res.value();
DDWAF_TRACE("Target {} matched parameter value {}", param.address, highlight);
cache.match =
condition_match{.args = {{.name = "resource"sv,
.resolved = generate_string_resource(resource.value),
.address = resource.address,
.key_path = convert_key_path(resource.key_path)},
{.name = "params"sv,
.resolved = highlight,
.address = param.address,
.key_path = param_kp}},
.highlights = {std::move(highlight)},
.operator_name = "cmdi_detector"sv,
.operator_value = {}};
return true;
}
}
return false;
}
}