Skip to content

Commit 34657d1

Browse files
Add Lua logging interface
1 parent 592a2d2 commit 34657d1

5 files changed

Lines changed: 262 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 via OsConfigLogInfo
53+
// ce.log.warning(message) -- log at warning level via OsConfigLogWarning
54+
// ce.log.error(message) -- log at error level via OsConfigLogError
55+
// ce.log.debug(message) -- log at debug level via OsConfigLogDebug
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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "LuaEvaluator.h"
55

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

@@ -598,6 +599,157 @@ TEST_F(LuaEvaluatorTest, Performance_MultipleEvaluations)
598599
}
599600
}
600601

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

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)