Skip to content

Commit 56892c8

Browse files
Merge branch 'dev' into robertwoj/38137096
2 parents 741693b + 48a7ac9 commit 56892c8

5 files changed

Lines changed: 306 additions & 3 deletions

File tree

src/modules/complianceengine/src/lib/LuaEvaluator.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,7 @@ void LuaEvaluator::SecureLuaEnvironment()
247247
{
248248
lua_newtable(L);
249249

250-
const std::vector<const char*> safeGlobals = {
251-
"print", "type", "tostring", "tonumber", "pairs", "ipairs", "next", "pcall", "xpcall", "select", "math"};
250+
const std::vector<const char*> safeGlobals = {"type", "tostring", "tonumber", "pairs", "ipairs", "next", "pcall", "xpcall", "select", "math"};
252251
const std::map<const char*, std::vector<const char*>> safeModuleFunctions = {
253252
{"string", {"byte", "char", "find", "format", "gsub", "len", "lower", "match", "gmatch", "rep", "reverse", "sub", "upper"}},
254253
{"table", {"concat", "insert", "remove", "sort"}}, {"io", {"lines"}}, {"os", {"time", "date", "clock", "difftime"}}};

src/modules/complianceengine/src/lib/LuaProcedures.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,75 @@ int LuaIndicatorsNonCompliant(lua_State* L)
528528
{
529529
return LuaIndicatorsAddIndicator(L, Status::NonCompliant);
530530
}
531+
532+
int LuaLogImpl(lua_State* L, LoggingLevel level)
533+
{
534+
// Fetch call context placed in registry by evaluator to access ContextInterface
535+
lua_pushstring(L, "lua_call_context");
536+
lua_gettable(L, LUA_REGISTRYINDEX);
537+
void* cc = lua_touserdata(L, -1);
538+
lua_pop(L, 1);
539+
if (!cc)
540+
{
541+
luaL_error(L, "internal error: missing call context");
542+
return 0;
543+
}
544+
auto* view = reinterpret_cast<LuaCallContext*>(cc);
545+
546+
// Message parameter
547+
constexpr int messageArgIndex = 1;
548+
if (lua_isnoneornil(L, messageArgIndex))
549+
{
550+
luaL_error(L, "expected a message");
551+
return 0;
552+
}
553+
if (!lua_isnone(L, messageArgIndex + 1))
554+
{
555+
luaL_error(L, "expected a single argument");
556+
return 0;
557+
}
558+
if (!lua_isstring(L, messageArgIndex))
559+
{
560+
luaL_error(L, "expected a string argument");
561+
return 0;
562+
}
563+
const char* message = lua_tostring(L, messageArgIndex);
564+
if (!message)
565+
{
566+
luaL_error(L, "expected a message");
567+
return 0;
568+
}
569+
if (!::strlen(message))
570+
{
571+
luaL_error(L, "message must not be empty");
572+
return 0;
573+
}
574+
575+
// The message is passed as the %s argument, never as the format string,
576+
// preventing format-string injection from Lua-supplied content.
577+
OsConfigLog(view->ctx.GetLogHandle(), level, "[Lua] %s", message);
578+
return 0;
579+
}
580+
581+
int LuaLogInfo(lua_State* L)
582+
{
583+
return LuaLogImpl(L, LoggingLevelInformational);
584+
}
585+
586+
int LuaLogWarning(lua_State* L)
587+
{
588+
return LuaLogImpl(L, LoggingLevelWarning);
589+
}
590+
591+
int LuaLogError(lua_State* L)
592+
{
593+
return LuaLogImpl(L, LoggingLevelError);
594+
}
595+
596+
int LuaLogDebug(lua_State* L)
597+
{
598+
return LuaLogImpl(L, LoggingLevelDebug);
599+
}
531600
} // anonymous namespace
532601

533602
void RegisterLuaProcedures(lua_State* L)
@@ -561,6 +630,29 @@ void RegisterLuaProcedures(lua_State* L)
561630
lua_pushcfunction(L, LuaSystemdCatConfig);
562631
lua_setfield(L, -2, "SystemdCatConfig");
563632

633+
// Get or create ce.log table
634+
lua_getfield(L, -1, "log");
635+
if (!lua_istable(L, -1))
636+
{
637+
lua_pop(L, 1); // pop non-table
638+
lua_newtable(L); // create log
639+
lua_pushvalue(L, -1);
640+
lua_setfield(L, -3, "log");
641+
642+
lua_pushcfunction(L, LuaLogInfo);
643+
lua_setfield(L, -2, "info");
644+
645+
lua_pushcfunction(L, LuaLogWarning);
646+
lua_setfield(L, -2, "warning");
647+
648+
lua_pushcfunction(L, LuaLogError);
649+
lua_setfield(L, -2, "error");
650+
651+
lua_pushcfunction(L, LuaLogDebug);
652+
lua_setfield(L, -2, "debug");
653+
}
654+
lua_pop(L, 1); // pop log table
655+
564656
// Get or create ce.indicators table
565657
lua_getfield(L, -1, "indicators");
566658
if (!lua_istable(L, -1))

src/modules/complianceengine/src/lib/LuaProcedures.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ struct LuaCallContext
4848
// Notes:
4949
// - Snapshot semantics: iterator holds shared_ptr<const FSCache>; unaffected by background refresh.
5050
// - Arguments outside unsigned 32-bit range produce Lua error.
51+
//
52+
// ce.log.info(message) -- log at informational level
53+
// ce.log.warning(message) -- log at warning level
54+
// ce.log.error(message) -- log at error level
55+
// ce.log.debug(message) -- log at debug level
56+
// message: string (required, non-empty) text to emit; prefixed with "[Lua] " in the log output.
57+
// Behavior:
58+
// - Routes to the OsConfigLog* macros using the log handle from the current call context.
59+
// - The message string is always a %s argument, never the format string (prevents format-string injection).
60+
// - Raises Lua error if message is missing, non-string, empty, or more than one argument is passed.
5161
void RegisterLuaProcedures(lua_State* L);
5262

5363
} // namespace ComplianceEngine

src/modules/complianceengine/tests/LuaEvaluatorTest.cpp

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
#include "LuaEvaluator.h"
55

66
#include "Indicators.h"
7+
#include "Logging.h"
78
#include "MockContext.h"
89
#include "Result.h"
910

11+
#include <fstream>
1012
#include <gtest/gtest.h>
13+
#include <iterator>
1114
#include <memory>
1215
#include <string>
1316

@@ -598,6 +601,199 @@ TEST_F(LuaEvaluatorTest, Performance_MultipleEvaluations)
598601
}
599602
}
600603

604+
// Test ce.log functions exist and can be called without error
605+
TEST_F(LuaEvaluatorTest, Log_Callable_AllLevels)
606+
{
607+
LuaEvaluator evaluator;
608+
609+
const std::string script = R"(
610+
ce.log.info("info message")
611+
ce.log.warning("warning message")
612+
ce.log.error("error message")
613+
ce.log.debug("debug message")
614+
return true
615+
)";
616+
617+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
618+
619+
ASSERT_TRUE(result.HasValue());
620+
EXPECT_EQ(result.Value(), Status::Compliant);
621+
}
622+
623+
// Test that print is no longer available in the restricted Lua environment
624+
TEST_F(LuaEvaluatorTest, Security_PrintBlocked)
625+
{
626+
LuaEvaluator evaluator;
627+
628+
const std::string script = "if print then return 'print available' else return true end";
629+
630+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
631+
632+
ASSERT_TRUE(result.HasValue());
633+
EXPECT_EQ(result.Value(), Status::Compliant);
634+
}
635+
636+
// Test that ce.log.info with no argument raises a Lua error
637+
TEST_F(LuaEvaluatorTest, Log_InvalidUsage_NoArgument)
638+
{
639+
LuaEvaluator evaluator;
640+
641+
const std::string script = R"(
642+
local ok, err = pcall(function() ce.log.info() end)
643+
if ok then
644+
return false, "expected error for missing argument"
645+
end
646+
return true, tostring(err)
647+
)";
648+
649+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
650+
651+
ASSERT_TRUE(result.HasValue());
652+
EXPECT_EQ(result.Value(), Status::Compliant);
653+
}
654+
655+
// Test that ce.log.info with a non-string, non-coercible argument raises a Lua error
656+
TEST_F(LuaEvaluatorTest, Log_InvalidUsage_NonStringArgument)
657+
{
658+
LuaEvaluator evaluator;
659+
660+
// Boolean false is not coercible to a string in Lua
661+
const std::string script = R"(
662+
local ok, err = pcall(function() ce.log.info(false) end)
663+
if ok then
664+
return false, "expected error for non-string argument"
665+
end
666+
return true, tostring(err)
667+
)";
668+
669+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
670+
671+
ASSERT_TRUE(result.HasValue());
672+
EXPECT_EQ(result.Value(), Status::Compliant);
673+
}
674+
675+
// Test that ce.log.info with more than one argument raises a Lua error
676+
TEST_F(LuaEvaluatorTest, Log_InvalidUsage_ExtraArguments)
677+
{
678+
LuaEvaluator evaluator;
679+
680+
const std::string script = R"(
681+
local ok, err = pcall(function() ce.log.info("msg", "extra") end)
682+
if ok then
683+
return false, "expected error for extra argument"
684+
end
685+
return true, tostring(err)
686+
)";
687+
688+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
689+
690+
ASSERT_TRUE(result.HasValue());
691+
EXPECT_EQ(result.Value(), Status::Compliant);
692+
}
693+
694+
// Test that ce.log.info with an empty string raises a Lua error
695+
TEST_F(LuaEvaluatorTest, Log_InvalidUsage_EmptyString)
696+
{
697+
LuaEvaluator evaluator;
698+
699+
const std::string script = R"(
700+
local ok, err = pcall(function() ce.log.info("") end)
701+
if ok then
702+
return false, "expected error for empty message"
703+
end
704+
return true, tostring(err)
705+
)";
706+
707+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
708+
709+
ASSERT_TRUE(result.HasValue());
710+
EXPECT_EQ(result.Value(), Status::Compliant);
711+
}
712+
713+
// E2E test: verify ce.log messages are written to the log file with the [Lua] prefix
714+
TEST_F(LuaEvaluatorTest, Log_E2E_WritesToLogFile)
715+
{
716+
const std::string logPath = mContext.GetTempdirPath() + "/lua_test.log";
717+
OsConfigLogHandle logHandle = OpenLog(logPath.c_str(), nullptr);
718+
ASSERT_NE(nullptr, logHandle);
719+
720+
const LoggingLevel savedLevel = GetLoggingLevel();
721+
const bool savedConsole = IsConsoleLoggingEnabled();
722+
SetLoggingLevel(LoggingLevelDebug);
723+
SetConsoleLoggingEnabled(false);
724+
725+
mContext.SetLogHandle(logHandle);
726+
727+
LuaEvaluator evaluator;
728+
const std::string script = R"(
729+
ce.log.info("info message")
730+
ce.log.warning("warning message")
731+
ce.log.error("error message")
732+
ce.log.debug("debug message")
733+
return true
734+
)";
735+
736+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
737+
738+
CloseLog(&logHandle);
739+
mContext.SetLogHandle(nullptr);
740+
SetLoggingLevel(savedLevel);
741+
SetConsoleLoggingEnabled(savedConsole);
742+
743+
ASSERT_TRUE(result.HasValue());
744+
EXPECT_EQ(result.Value(), Status::Compliant);
745+
746+
std::ifstream logFile(logPath);
747+
const std::string contents((std::istreambuf_iterator<char>(logFile)), std::istreambuf_iterator<char>());
748+
749+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] info message"));
750+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] warning message"));
751+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] error message"));
752+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] debug message"));
753+
}
754+
755+
// Test that format specifiers in log messages are emitted verbatim and not interpreted
756+
TEST_F(LuaEvaluatorTest, Log_E2E_FormatSpecifiersPassedVerbatim)
757+
{
758+
const std::string logPath = mContext.GetTempdirPath() + "/lua_fmt_test.log";
759+
OsConfigLogHandle logHandle = OpenLog(logPath.c_str(), nullptr);
760+
ASSERT_NE(nullptr, logHandle);
761+
762+
const LoggingLevel savedLevel = GetLoggingLevel();
763+
const bool savedConsole = IsConsoleLoggingEnabled();
764+
SetLoggingLevel(LoggingLevelDebug);
765+
SetConsoleLoggingEnabled(false);
766+
767+
mContext.SetLogHandle(logHandle);
768+
769+
LuaEvaluator evaluator;
770+
const std::string script = R"(
771+
ce.log.info("%d")
772+
ce.log.info("%s")
773+
ce.log.info("%n")
774+
ce.log.info("%%")
775+
return true
776+
)";
777+
778+
auto result = evaluator.Evaluate(script, mIndicators, mContext, Action::Audit);
779+
780+
CloseLog(&logHandle);
781+
mContext.SetLogHandle(nullptr);
782+
SetLoggingLevel(savedLevel);
783+
SetConsoleLoggingEnabled(savedConsole);
784+
785+
ASSERT_TRUE(result.HasValue());
786+
EXPECT_EQ(result.Value(), Status::Compliant);
787+
788+
std::ifstream logFile(logPath);
789+
const std::string contents((std::istreambuf_iterator<char>(logFile)), std::istreambuf_iterator<char>());
790+
791+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] %d"));
792+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] %s"));
793+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] %n"));
794+
EXPECT_THAT(contents, ::testing::HasSubstr("[Lua] %%"));
795+
}
796+
601797
// Test non-copyable nature of LuaEvaluator
602798
TEST_F(LuaEvaluatorTest, NonCopyable)
603799
{

src/modules/complianceengine/tests/MockContext.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ struct MockContext : public ComplianceEngine::ContextInterface
2222

2323
OsConfigLogHandle GetLogHandle() const override
2424
{
25-
return nullptr;
25+
return mLogHandle;
26+
}
27+
28+
void SetLogHandle(OsConfigLogHandle logHandle)
29+
{
30+
mLogHandle = logHandle;
2631
}
2732

2833
MockContext()
@@ -155,4 +160,5 @@ struct MockContext : public ComplianceEngine::ContextInterface
155160
std::vector<std::string> mTempfiles;
156161
std::map<std::string, std::string> mSpecialFilesMap;
157162
std::unique_ptr<ComplianceEngine::FilesystemScanner> mFsScannerp;
163+
OsConfigLogHandle mLogHandle{nullptr};
158164
};

0 commit comments

Comments
 (0)