Skip to content

Commit a2e3af5

Browse files
committed
BP: MB-41352: Prune audit log older than X seconds
If the configuration includes "audit_prune_age":<somenumber> the audit daemon will remove old audit files as part of checking if files should be rotated or not. If the value is set to 0 (or not present in the configuration) the audit daemon will not try to prune old audit logs. The file age is determined from the files "last write timestamp", and all files matching "hostname[*]-audit.log" will be subject for removal Change-Id: I34205e514e16beba0248e9b9ac6bfa3ba59a4195 Reviewed-on: https://review.couchbase.org/c/kv_engine/+/200038 Well-Formed: Restriction Checker Tested-by: Trond Norbye <[email protected]> Reviewed-by: Jim Walker <[email protected]>
1 parent 78b747e commit a2e3af5

File tree

8 files changed

+230
-7
lines changed

8 files changed

+230
-7
lines changed

auditd/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,9 @@ the following fields:
358358
one day. Minimum is 15 minutes)
359359
* rotate_size - number of bytes written to the file before rotating to a new
360360
file
361+
* prune_age - (optional field) Prune all audit log files older than the
362+
specified value (in seconds). (set to 0 (or remove) to disable the
363+
functionality).
361364
* buffered - should buffered file IO be used or not
362365
* disabled - list of event ids (numbers) containing those events that are NOT
363366
to be outputted to the audit log. This is depreciated in version 2 and has

auditd/src/audit.cc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,7 @@ void AuditImpl::consume_events() {
411411

412412
while (!stop_audit_consumer) {
413413
if (filleventqueue.empty()) {
414-
events_arrived.wait_for(
415-
lock,
416-
std::chrono::seconds(auditfile.get_seconds_to_rotation()));
414+
events_arrived.wait_for(lock, auditfile.get_sleep_time());
417415
if (filleventqueue.empty()) {
418416
// We timed out, so just rotate the files
419417
if (auditfile.maybe_rotate_files()) {

auditd/src/auditconfig.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,18 @@ AuditConfig::AuditConfig(const nlohmann::json& json) {
5656
}
5757
}
5858

59+
if (json.contains("prune_age")) {
60+
auto val = json["prune_age"].get<size_t>();
61+
if (val != 0) {
62+
*(prune_age.lock()) = std::chrono::seconds(val);
63+
}
64+
}
65+
5966
std::map<std::string, int> tags;
6067
tags["version"] = 1;
6168
tags["rotate_size"] = 1;
6269
tags["rotate_interval"] = 1;
70+
tags["prune_age"] = 1;
6371
tags["auditd_enabled"] = 1;
6472
tags["buffered"] = 1;
6573
tags["log_path"] = 1;
@@ -418,7 +426,7 @@ void AuditConfig::initialize_config(const nlohmann::json& json) {
418426
disabled_userids.swap(other.disabled_userids);
419427
event_states.swap(other.event_states);
420428
uuid.swap(other.uuid);
421-
429+
prune_age.swap(other.prune_age);
422430
version = other.version;
423431
}
424432

auditd/src/auditconfig.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
#include <nlohmann/json_fwd.hpp>
1414
#include <relaxed_atomic.h>
1515
#include <atomic>
16+
#include <chrono>
1617
#include <cinttypes>
1718
#include <mutex>
19+
#include <optional>
1820
#include <string>
1921
#include <unordered_map>
2022
#include <utility> // For std::pair
@@ -102,6 +104,10 @@ class AuditConfig {
102104
*/
103105
std::unique_ptr<AuditEventFilter> createAuditEventFilter(uint64_t rev);
104106

107+
[[nodiscard]] std::optional<std::chrono::seconds> get_prune_age() const {
108+
return *prune_age.lock();
109+
}
110+
105111
protected:
106112
void sanitize_path(std::string &path);
107113
void add_array(std::vector<uint32_t>& vec,
@@ -138,6 +144,9 @@ class AuditConfig {
138144
event_states;
139145
folly::Synchronized<std::string, std::mutex> uuid;
140146

147+
folly::Synchronized<std::optional<std::chrono::seconds>, std::mutex>
148+
prune_age;
149+
141150
const uint32_t min_file_rotation_time = 900; // 15 minutes
142151
const uint32_t max_file_rotation_time = 604800; // 1 week
143152
const size_t max_rotate_file_size = 500 * 1024 * 1024; // 500MB

auditd/src/auditfile.cc

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111
#include "auditfile.h"
1212

13+
#include <folly/ScopeGuard.h>
1314
#include <logger/logger.h>
1415
#include <memcached/isotime.h>
1516
#include <nlohmann/json.hpp>
@@ -18,14 +19,78 @@
1819
#include <platform/platform_time.h>
1920
#include <platform/strerror.h>
2021
#include <utilities/json_utilities.h>
21-
22-
#include <sys/stat.h>
2322
#include <algorithm>
24-
#include <cstring>
2523
#include <iostream>
2624
#include <sstream>
2725

26+
/// c++20 std::string::starts_with
27+
bool starts_with(std::string_view name, std::string_view prefix) {
28+
return name.find(prefix) == 0;
29+
}
30+
31+
/// c++20 std::string::ends_with
32+
bool ends_with(std::string_view name, std::string_view suffix) {
33+
const auto idx = name.rfind(suffix);
34+
if (idx == std::string_view::npos) {
35+
return false;
36+
}
37+
return (idx + suffix.size()) == name.length();
38+
}
39+
40+
void AuditFile::iterate_old_files(
41+
const std::function<void(const std::filesystem::path&)>& callback) {
42+
using namespace std::filesystem;
43+
for (const auto& p : directory_iterator(log_directory)) {
44+
try {
45+
const auto& path = p.path();
46+
if (starts_with(path.filename().generic_string(), hostname) &&
47+
ends_with(path.generic_string(), "-audit.log")) {
48+
callback(path);
49+
}
50+
} catch (const std::exception& e) {
51+
LOG_WARNING(
52+
"AuditFile::iterate_old_files(): Exception occurred "
53+
"while inspecting \"{}\": {}",
54+
p.path().generic_string(),
55+
e.what());
56+
}
57+
}
58+
}
59+
60+
void AuditFile::prune_old_audit_files() {
61+
using namespace std::chrono;
62+
using namespace std::filesystem;
63+
64+
const auto filesystem_now = file_time_type::clock::now();
65+
const auto now = steady_clock::now();
66+
if (!prune_age.has_value() || next_prune > now) {
67+
return;
68+
}
69+
70+
auto oldest = filesystem_now;
71+
72+
const auto then = filesystem_now - *prune_age;
73+
iterate_old_files([this, then, &oldest](const auto& path) {
74+
auto mtime = last_write_time(path);
75+
if (mtime < then) {
76+
remove(path);
77+
} else if (mtime < oldest) {
78+
oldest = mtime;
79+
}
80+
});
81+
82+
if (oldest == filesystem_now) {
83+
next_prune = now + *prune_age;
84+
} else {
85+
auto age = duration_cast<seconds>(filesystem_now - oldest);
86+
87+
// set next prune to a second after the oldest file expire
88+
next_prune = now + (*prune_age - age) + seconds(1);
89+
}
90+
}
91+
2892
bool AuditFile::maybe_rotate_files() {
93+
auto prune = folly::makeGuard([this] { prune_old_audit_files(); });
2994
if (is_open() && time_to_rotate_log()) {
3095
if (is_empty()) {
3196
// Given the audit log is empty on rotation instead of
@@ -66,6 +131,20 @@ uint32_t AuditFile::get_seconds_to_rotation() const {
66131
}
67132
}
68133

134+
std::chrono::seconds AuditFile::get_sleep_time() const {
135+
using namespace std::chrono;
136+
137+
const auto rotation = seconds{get_seconds_to_rotation()};
138+
if (!prune_age) {
139+
return rotation;
140+
}
141+
const auto now = steady_clock::now();
142+
if (next_prune <= now) {
143+
return seconds{0};
144+
}
145+
return std::min(rotation, duration_cast<seconds>(next_prune - now));
146+
}
147+
69148
bool AuditFile::time_to_rotate_log() const {
70149
if (rotate_interval) {
71150
cb_assert(open_time != 0);
@@ -267,6 +346,8 @@ void AuditFile::reconfigure(const AuditConfig &config) {
267346
set_log_directory(config.get_log_directory());
268347
max_log_size = config.get_rotate_size();
269348
buffered = config.is_buffered();
349+
prune_age = config.get_prune_age();
350+
next_prune = std::chrono::steady_clock::now();
270351
}
271352

272353
bool AuditFile::flush() {

auditd/src/auditfile.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@
1313
#include "auditconfig.h"
1414

1515
#include <nlohmann/json_fwd.hpp>
16+
#include <chrono>
1617
#include <cinttypes>
1718
#include <cstdio>
1819
#include <ctime>
20+
#include <filesystem>
21+
#include <functional>
1922
#include <memory>
23+
#include <optional>
2024
#include <string>
2125

2226
class AuditFile {
@@ -85,6 +89,10 @@ class AuditFile {
8589
*/
8690
uint32_t get_seconds_to_rotation() const;
8791

92+
[[nodiscard]] std::chrono::seconds get_sleep_time() const;
93+
94+
void prune_old_audit_files();
95+
8896
protected:
8997
bool open();
9098
bool time_to_rotate_log() const;
@@ -98,6 +106,15 @@ class AuditFile {
98106

99107
static time_t auditd_time();
100108

109+
/**
110+
* Iterate over "old" audit log files (named hostname-<timestamp>-audit.log)
111+
* and call the provided callback with the path
112+
*
113+
* @param callback
114+
*/
115+
void iterate_old_files(
116+
const std::function<void(const std::filesystem::path&)>& callback);
117+
101118
struct FileDeleter {
102119
void operator()(FILE* fp) {
103120
fclose(fp);
@@ -112,6 +129,13 @@ class AuditFile {
112129
size_t current_size = 0;
113130
size_t max_log_size = 20 * 1024 * 1024;
114131
uint32_t rotate_interval = 900;
132+
std::optional<std::chrono::seconds> prune_age;
133+
/// Iterating over all files in the directory and fetch their
134+
/// modification time may be "costly" so we don't want to run
135+
/// it too often.
136+
std::chrono::steady_clock::time_point next_prune =
137+
std::chrono::steady_clock::now();
138+
115139
bool buffered = true;
116140
};
117141

auditd/tests/auditconfig_test.cc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,15 @@ TEST_F(AuditConfigTest, TestSpecifyEventStates) {
500500
}
501501
}
502502
}
503+
504+
TEST_F(AuditConfigTest, AuditPruneAge) {
505+
config.initialize_config(json);
506+
EXPECT_FALSE(config.get_prune_age().has_value());
507+
json["prune_age"] = 0;
508+
config.initialize_config(json);
509+
EXPECT_FALSE(config.get_prune_age().has_value());
510+
json["prune_age"] = 1000;
511+
config.initialize_config(json);
512+
EXPECT_TRUE(config.get_prune_age().has_value());
513+
EXPECT_EQ(1000, config.get_prune_age().value().count());
514+
}

auditd/tests/auditfile_test.cc

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@
1111
#include <platform/dirutils.h>
1212

1313
#include "auditfile.h"
14+
#include <fmt/format.h>
1415
#include <folly/portability/GTest.h>
16+
#include <memcached/isotime.h>
1517
#include <nlohmann/json.hpp>
1618
#include <platform/platform_time.h>
19+
#include <platform/strerror.h>
1720
#include <time.h>
1821
#include <atomic>
1922
#include <cstring>
23+
#include <deque>
2024
#include <iostream>
2125
#include <map>
2226

@@ -323,3 +327,87 @@ TEST_F(AuditFileTest, MB53282) {
323327
auditfile.reconfigure(config);
324328
auditfile.test_mb53282();
325329
}
330+
331+
TEST_F(AuditFileTest, PruneFiles) {
332+
class MockAuditFile : public AuditFile {
333+
public:
334+
explicit MockAuditFile(const std::filesystem::path& logdir)
335+
: AuditFile("PruneFiles") {
336+
set_log_directory(logdir.generic_string());
337+
for (int ii = 0; ii < 10; ++ii) {
338+
createAuditLogFile(
339+
logdir, "PruneFiles", std::chrono::hours(ii));
340+
}
341+
};
342+
343+
static void createAuditLogFile(const std::filesystem::path& logdir,
344+
std::string_view hostname,
345+
std::chrono::seconds seconds) {
346+
using namespace std::filesystem;
347+
auto ts = ISOTime::generatetimestamp(
348+
time(nullptr) - seconds.count(), 0)
349+
.substr(0, 19);
350+
std::replace(ts.begin(), ts.end(), ':', '-');
351+
auto filename = fmt::format("{}-{}-audit.log", hostname, ts);
352+
auto path = logdir / fmt::format("{}-{}-audit.log", hostname, ts);
353+
FILE* fp = fopen(path.generic_string().c_str(), "w");
354+
if (!fp) {
355+
throw std::runtime_error(fmt::format(
356+
"createAuditLogFile: Failed to create {}: {}",
357+
path.generic_string(),
358+
cb_strerror()));
359+
}
360+
fclose(fp);
361+
auto ftime = file_time_type::clock::now() - seconds;
362+
last_write_time(path, ftime);
363+
}
364+
365+
void set_prune_age(std::chrono::seconds age) {
366+
using namespace std::chrono;
367+
prune_age = age;
368+
next_prune = steady_clock::now() - seconds(1);
369+
}
370+
371+
std::deque<std::filesystem::path> get_log_files() {
372+
std::deque<std::filesystem::path> ret;
373+
for (const auto& p :
374+
std::filesystem::directory_iterator(log_directory)) {
375+
if (is_regular_file(p.path())) {
376+
ret.push_back(p.path());
377+
}
378+
}
379+
380+
std::sort(ret.begin(), ret.end());
381+
return ret;
382+
}
383+
};
384+
385+
MockAuditFile auditfile(testdir);
386+
auto blueprint = auditfile.get_log_files();
387+
EXPECT_EQ(10, blueprint.size());
388+
389+
// We don't have a prune time, so all files should be there
390+
auditfile.prune_old_audit_files();
391+
EXPECT_EQ(blueprint, auditfile.get_log_files());
392+
393+
// If we set the prune time longer than all the files they should still
394+
// be there
395+
auditfile.set_prune_age(std::chrono::hours(9) + std::chrono::minutes{30});
396+
auditfile.prune_old_audit_files();
397+
EXPECT_EQ(blueprint, auditfile.get_log_files());
398+
399+
// set the prune time between the two last files
400+
auditfile.set_prune_age(std::chrono::hours(8) + std::chrono::minutes{30});
401+
auditfile.prune_old_audit_files();
402+
blueprint.pop_front();
403+
EXPECT_EQ(blueprint, auditfile.get_log_files());
404+
405+
// Verify that we nuke multiple old files by specifying a time
406+
// only leaving one file
407+
auditfile.set_prune_age(std::chrono::minutes{30});
408+
auditfile.prune_old_audit_files();
409+
while (blueprint.size() > 1) {
410+
blueprint.pop_front();
411+
}
412+
EXPECT_EQ(blueprint, auditfile.get_log_files());
413+
}

0 commit comments

Comments
 (0)