import CS aide-0.19.2-7.el9

This commit is contained in:
AlmaLinux RelEng Bot 2026-08-24 08:50:28 -04:00
parent a1c5816ac1
commit cadece6330
8 changed files with 1404 additions and 34 deletions

View File

@ -20,21 +20,28 @@
compared with the AIDE database. Prior to running a check manually,
ensure that the AIDE binary and database have not been modified
without your knowledge.
Caution!
With the default setup, an AIDE check is not run periodically as a
cron job. It cannot be guaranteed that the AIDE binaries, config
file and database are intact. It is not recommended that you run
automated AIDE checks without verifying AIDE yourself frequently.
In addition to that, AIDE does not implement any password or
encryption protection for its own files.
It is up to you how to put a file integrity checker to good effect
and how to set up automated checks if you think it adds a level of
safety (e.g. detecting failed/incomplete compromises or unauthorized
modification of special files). On a compromised system, the
intruder could disable the automated check. Or he could replace the
AIDE binary, config file and database easily when they are not
located on read-only media.
6) To schedule daily integrity checks, enable the systemd timer:
systemctl enable --now aide-check.timer
View results with: journalctl -u aide-check
Check timer status with: systemctl status aide-check.timer
The timer runs daily with low CPU/IO priority to minimize impact
on production workloads. It is disabled by default — only enable
it after initializing the database (steps 2-4).
Caution!
It cannot be guaranteed that the AIDE binaries, config file and
database are intact. It is not recommended that you run automated
AIDE checks without verifying AIDE yourself frequently. In addition
to that, AIDE does not implement any password or encryption
protection for its own files.
It is up to you how to put a file integrity checker to good effect.
On a compromised system, the intruder could disable the automated
check. Or he could replace the AIDE binary, config file and database
easily when they are not located on read-only media.

View File

@ -0,0 +1,657 @@
From f3e62eb87e0a0e9c6fd43c933670447c8ab0517a Mon Sep 17 00:00:00 2001
From: Cropi <alakatos@redhat.com>
Date: Thu, 28 May 2026 14:50:34 +0200
Subject: [PATCH] conf, report: add syslog_format config option
Re-implement the syslog_format option that existed as a Red Hat downstream
patch against aide 0.16 but was dropped during the rebase to 0.19.2.
Customers upgrading from RHEL 9.7 (aide 0.16) to RHEL 9.8 (aide 0.19.2)
received a fatal parse error on startup if their aide.conf contained
'syslog_format = true' (RHEL-178317).
The option is implemented as a new REPORT_FORMAT_SYSLOG value in the
existing report format module system, rather than as a standalone boolean,
which fits the 0.19.2 architecture cleanly.
syslog_format = yes/true is equivalent to report_format = syslog
Both spellings are accepted; last-write wins.
When active, the standard multi-line report is replaced with a compact
semicolon-delimited format where every file event is one line:
AIDE found differences between database and filesystem!!
summary;total_number_of_files=N;added_files=N;removed_files=N;changed_files=N
file=/usr/sbin/sshd;Mtime_old=...;Mtime_new=...;SHA256_old=...;SHA256_new=...
dir=/etc/cron.d; added
file=/usr/bin/old; removed
Each line is emitted with a single report_printf() call so that when used
with report_url=syslog:<FACILITY> exactly one syslog message is produced
per file event.
The module implements its own unconditional tree walker (not gated on
report_level) so added and removed entries are always included, matching
the original patch behaviour. ACL and xattr values are formatted directly
from db_line fields rather than through get_attribute_values() to avoid
embedded newlines breaking the single-line invariant. The original patch's
uninitialized 'char *A' in the ACL path is fixed.
Signed-off-by: Cropi <alakatos@redhat.com>
---
Makefile.am | 1 +
doc/aide.conf.5 | 28 ++++
include/conf_ast.h | 1 +
include/db_config.h | 1 +
include/report.h | 3 +
include/report_syslog.h | 28 ++++
src/aide.c | 1 +
src/conf_ast.c | 1 +
src/conf_eval.c | 8 +
src/conf_lex.l | 7 +
src/report.c | 16 +-
src/report_syslog.c | 338 ++++++++++++++++++++++++++++++++++++++++
12 files changed, 432 insertions(+), 1 deletion(-)
create mode 100644 include/report_syslog.h
create mode 100644 src/report_syslog.c
diff --git a/Makefile.am b/Makefile.am
index f78a96c..356b983 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -31,6 +31,7 @@ aide_SOURCES = src/aide.c include/aide.h \
include/report.h src/report.c \
include/report_plain.h src/report_plain.c \
include/report_json.h src/report_json.c \
+ include/report_syslog.h src/report_syslog.c \
include/conf_ast.h src/conf_ast.c \
include/conf_eval.h src/conf_eval.c \
include/conf_lex.h src/conf_lex.l \
diff --git a/doc/aide.conf.5 b/doc/aide.conf.5
index 4ad9f3e..5366be4 100644
--- a/doc/aide.conf.5
+++ b/doc/aide.conf.5
@@ -266,6 +266,34 @@ The report format to use. The available report formats are as follows:
\fBjson\fP: Print report in json machine-readable format.
.RE
+.IP "syslog_format (type: bool, default: \fBno\fR)"
+Valid values are \fByes\fR, \fBtrue\fR, \fBno\fR and \fBfalse\fR.
+
+When enabled, the standard multi-line report is replaced with a compact
+semicolon-delimited format where every file event is emitted as a single line.
+This ensures that when used with \fBreport_url=syslog:\fILOG_FACILITY\fR, exactly
+one syslog message is produced per changed, added, or removed file.
+
+Output starts with a header line when differences are found:
+.nf
+AIDE found differences between database and filesystem!!
+.fi
+Followed by a summary line:
+.nf
+summary;total_number_of_files=\fIN\fP;added_files=\fIN\fP;removed_files=\fIN\fP;changed_files=\fIN\fP
+.fi
+Then one line per changed, added, or removed entry:
+.nf
+file=/usr/sbin/sshd;Mtime_old=...;Mtime_new=...;SHA256_old=...;SHA256_new=...
+dir=/etc/cron.d; added
+file=/usr/bin/old; removed
+.fi
+
+The maximum size of a single syslog message depends on the syslog daemon
+(typically 1\(en8\ KB). Lines exceeding this limit will be silently truncated
+by the syslog daemon. This is not controlled by AIDE.
+
+The \fBreport_summarize_changes\fR option has no effect in this format.
.IP "report_base16 (type: bool, default: \fBfalse\fR, added in AIDE v0.17)"
Base16 encode the checksums in the report. The default is to
diff --git a/include/conf_ast.h b/include/conf_ast.h
index 8892a05..424f734 100644
--- a/include/conf_ast.h
+++ b/include/conf_ast.h
@@ -53,6 +53,7 @@ typedef enum config_option {
REPORT_FORMAT_OPTION,
LIMIT_CMDLINE_OPTION,
NUM_WORKERS,
+ SYSLOG_FORMAT_OPTION,
} config_option;
typedef struct {
diff --git a/include/db_config.h b/include/db_config.h
index 4173a4b..363631e 100644
--- a/include/db_config.h
+++ b/include/db_config.h
@@ -133,6 +133,7 @@ typedef struct db_config {
int report_detailed_init;
int report_base16;
int report_quiet;
+ int syslog_format;
bool report_append;
DB_ATTR_TYPE report_ignore_added_attrs;
diff --git a/include/report.h b/include/report.h
index 2ec3539..b2efa55 100644
--- a/include/report.h
+++ b/include/report.h
@@ -48,6 +48,7 @@ typedef enum {
REPORT_FORMAT_UNKNOWN = 0,
REPORT_FORMAT_PLAIN = 1,
REPORT_FORMAT_JSON = 2,
+ REPORT_FORMAT_SYSLOG = 3,
} REPORT_FORMAT;
extern const ATTRIBUTE report_attrs_order[];
@@ -138,6 +139,8 @@ typedef struct report_format_module {
void (*print_report_summary)(report_t*);
} report_format_module;
+DB_ATTR_TYPE get_report_attributes(seltree*, report_t*);
+
char* get_file_type_string(mode_t);
char* get_summarize_changes_string(report_t*, seltree*);
char* get_summary_string(report_t*);
diff --git a/include/report_syslog.h b/include/report_syslog.h
new file mode 100644
index 0000000..4da9ae4
--- /dev/null
+++ b/include/report_syslog.h
@@ -0,0 +1,28 @@
+/*
+ * AIDE (Advanced Intrusion Detection Environment)
+ *
+ * Copyright (C) 2025 Hannes von Haugwitz
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef _REPORT_SYSLOG_H_INCLUDED
+#define _REPORT_SYSLOG_H_INCLUDED
+
+#include "report.h"
+
+extern report_format_module report_module_syslog;
+
+#endif
diff --git a/src/aide.c b/src/aide.c
index 6f728a9..f9a9cd4 100644
--- a/src/aide.c
+++ b/src/aide.c
@@ -439,6 +439,7 @@ static void setdefaults_before_config(void)
conf->report_detailed_init=0;
conf->report_base16=0;
conf->report_quiet=0;
+ conf->syslog_format=0;
conf->report_append=false;
conf->report_ignore_added_attrs = 0;
conf->report_ignore_removed_attrs = 0;
diff --git a/src/conf_ast.c b/src/conf_ast.c
index 366bc3f..7b66aed 100644
--- a/src/conf_ast.c
+++ b/src/conf_ast.c
@@ -58,6 +58,7 @@ config_option_t config_options[] = {
{ REPORT_FORMAT_OPTION, NULL, NULL },
{ LIMIT_CMDLINE_OPTION, "limit", "Limit" },
{ NUM_WORKERS, NULL, NULL },
+ { SYSLOG_FORMAT_OPTION, NULL, NULL },
};
static ast* new_ast_node(void) {
diff --git a/src/conf_eval.c b/src/conf_eval.c
index 5774ce6..bb39610 100644
--- a/src/conf_eval.c
+++ b/src/conf_eval.c
@@ -264,6 +264,9 @@ static void eval_config_statement(config_option_statement statement, int linenum
REPORT_FORMAT report_format = get_report_format(str);
if (report_format != REPORT_FORMAT_UNKNOWN) {
conf->report_format = report_format;
+ for (list *l = conf->report_urls; l; l = l->next) {
+ ((report_t *)l->data)->format = report_format;
+ }
LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_CONFIG, "set 'report_format' option to '%s' (raw: %d)", str, report_format)
} else {
LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "invalid report format: '%s'", str);
@@ -315,6 +318,11 @@ static void eval_config_statement(config_option_statement statement, int linenum
LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_NOTICE, "'num_workers' option already set (ignore new value '%s')", str)
}
break;
+ case SYSLOG_FORMAT_OPTION:
+ b = string_expression_to_bool(statement.e, linenumber, filename, linebuf);
+ conf->syslog_format = b;
+ LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_CONFIG, "set 'syslog_format' to '%s'", btoa(b))
+ break;
}
}
diff --git a/src/conf_lex.l b/src/conf_lex.l
index 877a125..1a9654f 100644
--- a/src/conf_lex.l
+++ b/src/conf_lex.l
@@ -362,6 +362,13 @@ LOG_LEVEL lex_log_level = LOG_LEVEL_DEBUG;
BEGIN (STRINGEQHUNT);
return (CONFIGOPTION);
}
+<CONFIG>"syslog_format" {
+ LOG_LEX_TOKEN(lex_log_level, CONFIGOPTION (SYSLOG_FORMAT_OPTION), conftext)
+ conflval.option = SYSLOG_FORMAT_OPTION;
+ BEGIN (STRINGEQHUNT);
+ return (CONFIGOPTION);
+}
+
<CONFIG>"report_level" {
LOG_LEX_TOKEN(lex_log_level, CONFIGOPTION (REPORT_LEVEL_OPTION), conftext)
diff --git a/src/report.c b/src/report.c
index 9e4d0d0..f87754c 100644
--- a/src/report.c
+++ b/src/report.c
@@ -59,6 +59,7 @@
#include "report.h"
#include "report_plain.h"
#include "report_json.h"
+#include "report_syslog.h"
/*for locale support*/
#include "locale-aide.h"
/*for locale support*/
@@ -146,6 +147,7 @@ struct report_format {
static struct report_format report_format_array[] = {
{ REPORT_FORMAT_PLAIN, "plain" },
{ REPORT_FORMAT_JSON, "json" },
+ { REPORT_FORMAT_SYSLOG, "syslog" },
{ REPORT_FORMAT_UNKNOWN, NULL }
};
@@ -509,6 +511,15 @@ bool init_report_urls(void) {
}
}
+ }
+ /* syslog_format is a downstream-only option that must win over report_format
+ * regardless of declaration order in the config file. Enforce it here,
+ * after the entire config AST has been evaluated, so no subsequent
+ * report_format setting can accidentally override the user's intent. */
+ if (conf->syslog_format) {
+ for (l=conf->report_urls; l; l=l->next) {
+ ((report_t *)l->data)->format = REPORT_FORMAT_SYSLOG;
+ }
}
return true;
}
@@ -677,7 +688,7 @@ char* get_summarize_changes_string(report_t* report, seltree* node) {
-static DB_ATTR_TYPE get_report_attributes(seltree* node, report_t *report) {
+DB_ATTR_TYPE get_report_attributes(seltree* node, report_t *report) {
db_line* oline = node->old_data;
db_line* nline = node->new_data;
DB_ATTR_TYPE attrs = node->changed_attrs;
@@ -966,6 +977,9 @@ int gen_report(seltree* node) {
case REPORT_FORMAT_JSON:
print_report(report, node, report_module_json);
break;
+ case REPORT_FORMAT_SYSLOG:
+ print_report(report, node, report_module_syslog);
+ break;
case REPORT_FORMAT_UNKNOWN:
/* skip unknown report format */
break;
diff --git a/src/report_syslog.c b/src/report_syslog.c
new file mode 100644
index 0000000..920e927
--- /dev/null
+++ b/src/report_syslog.c
@@ -0,0 +1,338 @@
+/*
+ * AIDE (Advanced Intrusion Detection Environment)
+ *
+ * Copyright (C) 2025 Hannes von Haugwitz
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "aide.h"
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include "attributes.h"
+#include "base64.h"
+#include "db.h"
+#include "db_line.h"
+#include "hashsum.h"
+#include "report.h"
+#include "report_syslog.h"
+#include "seltree.h"
+#include "seltree_struct.h"
+#include "tree.h"
+#include "util.h"
+
+/* Returns the compact syslog file-type prefix, or NULL when mode is unknown
+ * (mode & S_IFMT == 0), in which case callers omit the "type=" prefix. */
+static const char *get_syslog_type_prefix(mode_t mode) {
+ switch (mode & S_IFMT) {
+ case S_IFREG: return "file";
+ case S_IFDIR: return "dir";
+ case S_IFLNK: return "link";
+ case S_IFBLK: return "blockd";
+ case S_IFCHR: return "chard";
+#ifdef S_IFIFO
+ case S_IFIFO: return "fifo";
+#endif
+#ifdef S_IFSOCK
+ case S_IFSOCK: return "socket";
+#endif
+ case 0: return NULL;
+ default: return "unknown";
+ }
+}
+
+/* Returns the key name for attribute a. attr_sizeg is the only exception:
+ * its details_string is "Size (>)" which contains '>', so we substitute
+ * "Size" to match the original patch's explicit details_string[] change. */
+static const char *get_attr_key(ATTRIBUTE a) {
+ if (a == attr_sizeg) {
+ return "Size";
+ }
+ return attributes[a].details_string;
+}
+
+#ifdef WITH_XATTR
+
+#define SYSLOG_PRINTABLE_XATTR_VALS \
+ "0123456789" \
+ "abcdefghijklmnopqrstuvwxyz" \
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
+ ".-_:;,[]{}<>()!@#$%^&*|\\/?~"
+
+/* Appends one side's xattr values to stream in compact syslog format.
+ * Format: ;XAttrs_<side>=|[1]key=val|[2]key=val| */
+static void build_xattrs_compact(FILE *stream, db_line *line, const char *side) {
+ xattrs_type *xattrs = line ? line->xattrs : NULL;
+
+ if (!xattrs || xattrs->num == 0) {
+ fprintf(stream, ";XAttrs_%s=|num=0|", side);
+ return;
+ }
+
+ fprintf(stream, ";XAttrs_%s=|num=%zu|", side, xattrs->num);
+
+ for (size_t i = 0; i < xattrs->num; i++) {
+ const char *key = xattrs->ents[i].key;
+ const char *val = (const char *)xattrs->ents[i].val;
+ size_t vsz = xattrs->ents[i].vsz;
+
+ /* Check printability (replicates xstrnspn logic from report.c). */
+ size_t plen = 0;
+ while (plen < vsz && strchr(SYSLOG_PRINTABLE_XATTR_VALS, val[plen]))
+ plen++;
+ bool printable = (plen == vsz) || (plen == vsz - 1 && val[plen] == '\0');
+
+ if (printable) {
+ fprintf(stream, "[%zu]%s=%s|", i + 1, key, val);
+ } else {
+ char *b64 = encode_base64((byte *)xattrs->ents[i].val, vsz);
+ fprintf(stream, "[%zu]%s<=>%s|", i + 1, key, b64 ? b64 : "");
+ free(b64);
+ }
+ }
+}
+#endif /* WITH_XATTR */
+
+#ifdef WITH_POSIX_ACL
+/* Appends one side's ACL to stream in compact syslog format.
+ * Format: ;ACL_<side>=A:<acl_a_newlines_as_spaces>|D:<acl_d_newlines_as_spaces>
+ *
+ * Both A and D are initialized to "<NONE>" before the conditionals, fixing
+ * the uninitialized-pointer bug in the original 0.16 patch. */
+static void build_acl_compact(FILE *stream, db_line *line, const char *side) {
+ acl_type *acl = line ? line->acl : NULL;
+
+ const char *A = "<NONE>";
+ const char *D = "<NONE>";
+ if (acl) {
+ if (acl->acl_a) { A = acl->acl_a; }
+ if (acl->acl_d) { D = acl->acl_d; }
+ }
+
+ /* Write A component, replacing newlines with spaces. */
+ fprintf(stream, ";ACL_%s=A:", side);
+ for (const char *p = A; *p; p++) {
+ fputc(*p == '\n' ? ' ' : *p, stream);
+ }
+
+ /* Write D component, replacing newlines with spaces. */
+ fprintf(stream, "|D:");
+ for (const char *p = D; *p; p++) {
+ fputc(*p == '\n' ? ' ' : *p, stream);
+ }
+}
+#endif /* WITH_POSIX_ACL */
+
+/* Assembles a complete syslog line for one file event into *out.
+ *
+ * Invariant: this function never calls report_printf(). The caller emits
+ * the result with exactly one report_printf() call to avoid fragmenting
+ * syslog messages (each report_printf to url_syslog calls vsyslog() once).
+ *
+ * Cases:
+ * oline != NULL && nline != NULL → changed entry
+ * oline == NULL → added entry
+ * nline == NULL → removed entry
+ *
+ * *out is heap-allocated by open_memstream; caller must free() it. */
+static void build_syslog_line(report_t *report, db_line *oline, db_line *nline,
+ DB_ATTR_TYPE attrs, char **out) {
+ db_line *ref = nline ? nline : oline;
+ const char *type = get_syslog_type_prefix(ref->perm);
+
+ char *buf = NULL;
+ size_t bufsz = 0;
+ FILE *stream = open_memstream(&buf, &bufsz);
+
+ /* Write "type=path" prefix. */
+ if (type) {
+ fprintf(stream, "%s=%s", type, ref->filename);
+ } else {
+ fprintf(stream, "%s", ref->filename);
+ }
+
+ if (oline && nline) {
+ /* Changed entry: emit only differing attributes. */
+ for (int j = 0; j < report_attrs_order_length; j++) {
+ ATTRIBUTE a = report_attrs_order[j];
+
+ switch (a) {
+ case attr_allhashsums:
+ /* Expand to each compiled-in hash, mirroring print_dbline_attrs(). */
+ for (int i = 0; i < num_hashes; i++) {
+ if (!(ATTR(hashsums[i].attribute) & attrs)) { continue; }
+ const char *key = get_attr_key(hashsums[i].attribute);
+ if (!key) { continue; }
+ char **oval = NULL, **nval = NULL;
+ get_attribute_values(ATTR(hashsums[i].attribute), oline, &oval, report);
+ get_attribute_values(ATTR(hashsums[i].attribute), nline, &nval, report);
+ fprintf(stream, ";%s_old=%s;%s_new=%s",
+ key, oval ? oval[0] : "", key, nval ? nval[0] : "");
+ if (oval) { free(oval[0]); free(oval); }
+ if (nval) { free(nval[0]); free(nval); }
+ }
+ break;
+
+ case attr_size:
+ /* attr_size and attr_sizeg share this slot in report_attrs_order. */
+ if (ATTR(attr_size) & attrs) {
+ const char *key = get_attr_key(attr_size);
+ if (key) {
+ char **oval = NULL, **nval = NULL;
+ get_attribute_values(ATTR(attr_size), oline, &oval, report);
+ get_attribute_values(ATTR(attr_size), nline, &nval, report);
+ fprintf(stream, ";%s_old=%s;%s_new=%s",
+ key, oval ? oval[0] : "", key, nval ? nval[0] : "");
+ if (oval) { free(oval[0]); free(oval); }
+ if (nval) { free(nval[0]); free(nval); }
+ }
+ }
+ if (ATTR(attr_sizeg) & attrs) {
+ const char *key = get_attr_key(attr_sizeg); /* returns "Size" */
+ if (key) {
+ char **oval = NULL, **nval = NULL;
+ get_attribute_values(ATTR(attr_sizeg), oline, &oval, report);
+ get_attribute_values(ATTR(attr_sizeg), nline, &nval, report);
+ fprintf(stream, ";%s_old=%s;%s_new=%s",
+ key, oval ? oval[0] : "", key, nval ? nval[0] : "");
+ if (oval) { free(oval[0]); free(oval); }
+ if (nval) { free(nval[0]); free(nval); }
+ }
+ }
+ break;
+
+ default:
+ if (!(ATTR(a) & attrs)) { break; }
+#ifdef WITH_XATTR
+ if (a == attr_xattrs) {
+ build_xattrs_compact(stream, oline, "old");
+ build_xattrs_compact(stream, nline, "new");
+ break;
+ }
+#endif
+#ifdef WITH_POSIX_ACL
+ if (a == attr_acl) {
+ build_acl_compact(stream, oline, "old");
+ build_acl_compact(stream, nline, "new");
+ break;
+ }
+#endif
+ {
+ const char *key = get_attr_key(a);
+ if (!key) { break; }
+ char **oval = NULL, **nval = NULL;
+ get_attribute_values(ATTR(a), oline, &oval, report);
+ get_attribute_values(ATTR(a), nline, &nval, report);
+ fprintf(stream, ";%s_old=%s;%s_new=%s",
+ key, oval ? oval[0] : "", key, nval ? nval[0] : "");
+ if (oval) { free(oval[0]); free(oval); }
+ if (nval) { free(nval[0]); free(nval); }
+ }
+ break;
+ }
+ }
+ } else if (!oline) {
+ fprintf(stream, "; added");
+ } else {
+ fprintf(stream, "; removed");
+ }
+
+ fclose(stream);
+ *out = buf;
+}
+
+/* Emits exactly one syslog line for a file event. */
+static void emit_syslog_entry(report_t *report, db_line *oline, db_line *nline,
+ DB_ATTR_TYPE attrs) {
+ char *line = NULL;
+ build_syslog_line(report, oline, nline, attrs, &line);
+ report_printf(report, "%s\n", line);
+ free(line);
+}
+
+/* Unconditional tree walker — does not gate added/removed on report->level,
+ * matching the original patch's print_syslog_format() behavior. */
+static void syslog_walk_tree(report_t *report, seltree *node) {
+ pthread_rwlock_rdlock(&node->rwlock);
+
+ if (node->checked & NODE_CHANGED) {
+ emit_syslog_entry(report, node->old_data, node->new_data,
+ get_report_attributes(node, report));
+ }
+ if (node->checked & NODE_ADDED) {
+ emit_syslog_entry(report, NULL, node->new_data,
+ node->new_data->attr & ~report->ignore_added_attrs);
+ }
+ if (node->checked & NODE_REMOVED) {
+ emit_syslog_entry(report, node->old_data, NULL,
+ node->old_data->attr & ~report->ignore_removed_attrs);
+ }
+
+ for (tree_node *x = tree_walk_first(node->children); x != NULL; x = tree_walk_next(x)) {
+ syslog_walk_tree(report, tree_get_data(x));
+ }
+
+ pthread_rwlock_unlock(&node->rwlock);
+}
+
+/* ── Module callbacks ─────────────────────────────────────────────────── */
+
+static void noop_header(report_t *report) { (void)report; }
+static void noop_footer(report_t *report) { (void)report; }
+static void noop_databases(report_t *report) { (void)report; }
+static void noop_config_options(report_t *report) { (void)report; }
+static void noop_report_options(report_t *report) { (void)report; }
+static void noop_starttime_version(report_t *r, const char *t, const char *v) { (void)r; (void)t; (void)v; }
+static void noop_endtime_runtime(report_t *r, const char *t, long rt) { (void)r; (void)t; (void)rt; }
+static void noop_new_database_written(report_t *report) { (void)report; }
+static void noop_entries(report_t *r, seltree *n, const int f) { (void)r; (void)n; (void)f; }
+static void noop_diff_attrs(report_t *report) { (void)report; }
+static void noop_summary(report_t *report) { (void)report; }
+
+/* Emits header + summary when there are differences. Two separate
+ * report_printf() calls — each becomes one syslog message. */
+static void syslog_outline(report_t *report) {
+ if (report->nadd || report->nrem || report->nchg) {
+ report_printf(report, "AIDE found differences between database and filesystem!!\n");
+ report_printf(report,
+ "summary;total_number_of_files=%ld;added_files=%ld;"
+ "removed_files=%ld;changed_files=%ld\n",
+ report->ntotal, report->nadd, report->nrem, report->nchg);
+ }
+}
+
+static void syslog_details(report_t *report, seltree *node) {
+ syslog_walk_tree(report, node);
+}
+
+report_format_module report_module_syslog = {
+ .print_report_config_options = noop_config_options,
+ .print_report_databases = noop_databases,
+ .print_report_details = syslog_details,
+ .print_report_diff_attrs_entries = noop_diff_attrs,
+ .print_report_endtime_runtime = noop_endtime_runtime,
+ .print_report_entries = noop_entries,
+ .print_report_footer = noop_footer,
+ .print_report_header = noop_header,
+ .print_report_new_database_written = noop_new_database_written,
+ .print_report_outline = syslog_outline,
+ .print_report_report_options = noop_report_options,
+ .print_report_starttime_version = noop_starttime_version,
+ .print_report_summary = noop_summary,
+};
--
2.54.0

View File

@ -0,0 +1,10 @@
[Unit]
Description=AIDE file integrity check
Documentation=man:aide(1) man:aide.conf(5)
[Service]
Type=oneshot
ExecStart=/usr/sbin/aide --check
SuccessExitStatus=0 1 2 3 4 5 6 7
Nice=19
IOSchedulingClass=idle

11
SOURCES/aide-check.timer Normal file
View File

@ -0,0 +1,11 @@
[Unit]
Description=Daily AIDE file integrity check
Documentation=man:aide(1) man:aide.conf(5)
[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true
[Install]
WantedBy=timers.target

View File

@ -0,0 +1,66 @@
diff --git a/src/conf_eval.c b/src/conf_eval.c
index 5774ce6..6503709 100644
--- a/src/conf_eval.c
+++ b/src/conf_eval.c
@@ -580,9 +580,9 @@ static void include_file(const char* file, bool execute, int include_depth, char
}
}
-void check_permissions(const char* path, struct stat *st, int linenumber, char *filename, char* linebuf) {
+static void check_permissions(const char* path, struct stat *st, const char *directive, int linenumber, char *filename, char* linebuf) {
if ((st->st_uid != geteuid() && st->st_uid != 0) || (st->st_mode & 002) != 0 || (st->st_mode & 020) != 0) {
- LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'@@x_include': bad ownership or modes for '%s' (please ensure it is neither group- nor world-writable and owned by the current user or root)", path)
+ LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'%s': bad ownership or modes for '%s' (please ensure it is neither group- nor world-writable and owned by the current user or root)", directive, path)
exit(INVALID_CONFIGURELINE_ERROR);
}
}
@@ -611,13 +611,13 @@ static void include_directory(const char* dir, const char* rx, bool execute, cha
struct stat fs;
- if (execute) {
- if (stat(dir,&fs) == -1) {
- LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'@@x_include': stat for '%s' failed: %s", dir, strerror(errno))
- exit(INVALID_CONFIGURELINE_ERROR);
- }
- check_permissions(dir, &fs, linenumber, filename, linebuf);
+ /* stat() follows symlinks; we intentionally check the target's ownership
+ * and mode rather than the symlink node itself */
+ if (stat(dir,&fs) == -1) {
+ LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'%s': stat for '%s' failed: %s", execute?"@@x_include":"@@include", dir, strerror(errno))
+ exit(INVALID_CONFIGURELINE_ERROR);
}
+ check_permissions(dir, &fs, execute?"@@x_include":"@@include", linenumber, filename, linebuf);
n = scandir(dir, &namelist, dirfilter, alphasort);
if (n == -1) {
@@ -660,9 +660,8 @@ static void include_directory(const char* dir, const char* rx, bool execute, cha
log_msg(LOG_LEVEL_DEBUG,"%s: skip '%s' (reason: file name does not match regex '%s')", dir, namelist[i]->d_name, rx);
} else {
int exec = execute && S_IXUSR&fs.st_mode;
- if (exec) {
- check_permissions(filepath, &fs, linenumber, filename, linebuf);
- }
+ /* pass directive name (not exec flag) so the error names the directive the user wrote */
+ check_permissions(filepath, &fs, execute?"@@x_include":"@@include", linenumber, filename, linebuf);
log_msg(LOG_LEVEL_CONFIG,"%s: %s '%s'", dir, exec?"execute":"include", namelist[i]->d_name);
include_file(filepath, exec, include_depth, nested_rule_prefix);
}
@@ -701,14 +700,15 @@ static void eval_include_statement(include_statement statement, int include_dept
} else {
struct stat fs;
if (lstat(path,&fs) == -1) {
- LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'@@include': lstat for '%s' failed: %s", path, strerror(errno))
+ LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'%s': lstat for '%s' failed: %s", statement.execute?"@@x_include":"@@include", path, strerror(errno))
exit(INVALID_CONFIGURELINE_ERROR);
}
if (S_ISREG(fs.st_mode)) {
+ check_permissions(path, &fs, statement.execute?"@@x_include":"@@include", linenumber, filename, linebuf);
LOG_CONFIG_FORMAT_LINE_PREFIX(LOG_LEVEL_CONFIG, "include file '%s' (depth: %d)", path, include_depth)
include_file(path, statement.execute && S_IXUSR&fs.st_mode, include_depth, rule_prefix);
} else {
- LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'@@include': '%s' is not a regular file", path);
+ LOG_CONFIG_FORMAT_LINE(LOG_LEVEL_ERROR, "'%s': '%s' is not a regular file", statement.execute?"@@x_include":"@@include", path);
exit(INVALID_CONFIGURELINE_ERROR);
}
}

588
SOURCES/aide-migrate-config Executable file
View File

@ -0,0 +1,588 @@
#!/bin/bash
# aide-migrate-config -- migrate AIDE configuration files from pre-0.19 syntax
#
# Usage: aide-migrate-config [--dry-run] [--skip-init] <config-file>
#
# --dry-run Report what would change; do not modify any file.
# --skip-init Migrate config files only; do not reinitialise the database.
# <config-file> Path to the main AIDE config (e.g. /etc/aide.conf).
set -euo pipefail
# ---------------------------------------------------------------------------
# Globals
# ---------------------------------------------------------------------------
readonly SCRIPT="$(basename "$0")"
readonly TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
DRY_RUN=false
SKIP_INIT=false
REINIT_NEEDED=false
# Parallel arrays: BACKUP_ORIG[i] → BACKUP_COPY[i]
BACKUP_ORIG=()
BACKUP_COPY=()
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
usage() {
echo "Usage: $SCRIPT [--dry-run] [--skip-init] <config-file>" >&2
exit 1
}
die() {
echo "$SCRIPT: error: $*" >&2
exit 1
}
info() {
echo "$SCRIPT: $*" >&2
}
# log_action MESSAGE
# Log a migration action. Prefixes with "would " in dry-run mode.
log_action() {
if $DRY_RUN; then
info " would $*"
else
info " $*"
fi
}
# config_has FILE KEY
# Return 0 if FILE contains a KEY= directive.
config_has() {
local file="$1" key="$2"
grep -qE "^[[:space:]]*${key}[[:space:]]*=" "$file"
}
# config_del FILE KEY
# Delete all lines matching KEY= from FILE in-place.
config_del() {
local file="$1" key="$2"
sed -E -i "/^[[:space:]]*${key}[[:space:]]*=/d" "$file"
}
# config_rename FILE OLD_KEY NEW_KEY
# Rename OLD_KEY= to NEW_KEY= in FILE in-place.
config_rename() {
local file="$1" old_key="$2" new_key="$3"
sed -E -i "s/^([[:space:]]*)${old_key}([[:space:]]*=)/\1${new_key}\2/" "$file"
}
# append_setting FILE CONTENT
# Append "CONTENT\n" to FILE, guaranteeing it starts on a fresh line.
# No-op if the key extracted from CONTENT already exists in FILE.
append_setting() {
local file="$1" content="$2"
local key="${content%%=*}"
config_has "$file" "$key" && return 0
if [[ -s "$file" ]] && [[ "$(tail -c1 "$file" | wc -l)" -eq 0 ]]; then
printf '\n' >> "$file"
fi
printf '%s\n' "$content" >> "$file"
}
# backup_file FILE
# Create a timestamped backup of FILE. No-op in dry-run mode.
backup_file() {
local file="$1"
local backup="${file}.bak.${TIMESTAMP}"
if $DRY_RUN; then
return 0
fi
cp -p -- "$file" "$backup"
BACKUP_ORIG+=("$file")
BACKUP_COPY+=("$backup")
info "backup: $backup"
}
# restore_backups
# Called by ERR trap; restores every backed-up file.
restore_backups() {
local i
for i in "${!BACKUP_ORIG[@]}"; do
cp -p -- "${BACKUP_COPY[$i]}" "${BACKUP_ORIG[$i]}" && \
info "restored: ${BACKUP_ORIG[$i]}" || \
info "WARNING: could not restore ${BACKUP_ORIG[$i]} from ${BACKUP_COPY[$i]}"
done
}
# ---------------------------------------------------------------------------
# Include-file discovery
# ---------------------------------------------------------------------------
# extract_includes CONFIG_FILE
# Print one absolute path per line for each file pulled in by @@include directives.
extract_includes() {
local config="$1"
local dir
dir="$(dirname "$config")"
grep -E '^[[:space:]]*@@include[[:space:]]' "$config" 2>/dev/null | \
while IFS= read -r line; do
# Strip the @@include keyword
local args
args="${line#*@@include}"
args="${args#"${args%%[! ]*}"}" # ltrim
# Count tokens to distinguish FILE vs DIRECTORY REGEX forms
local tok1 tok2
tok1="${args%% *}"
tok2="${args#* }"
if [[ "$tok1" == "$args" ]]; then
# Single token: @@include FILE
if [[ "$tok1" = /* ]]; then
echo "$tok1"
else
echo "${dir}/${tok1}"
fi
else
# Two tokens: @@include DIRECTORY REGEX
local incdir regex
incdir="$tok1"
regex="$tok2"
if [[ "$incdir" != /* ]]; then
incdir="${dir}/${incdir}"
fi
if [[ -d "$incdir" ]]; then
find "$incdir" -maxdepth 1 -type f -regex "$regex" | sort
fi
fi
done
}
# ---------------------------------------------------------------------------
# Config file migration
# ---------------------------------------------------------------------------
# needs_migration FILE
# Return 0 (true) if FILE contains any pattern requiring migration.
needs_migration() {
local f="$1"
# Removed/renamed config options
local _key
for _key in database grouped summarize_changes ignore_list report_attributes verbose; do
config_has "$f" "$_key" && return 0
done
# sql: value starts with sql: (PostgreSQL backend removed)
grep -qE '^[[:space:]]*[a-z_]+[[:space:]]*=[[:space:]]*sql:' "$f" && return 0
# any h character in report_ignore_e2fsattrs value
grep -qE '^[[:space:]]*report_ignore_e2fsattrs[[:space:]]*=.*h' "$f" && return 0
# removed/deprecated hashsums; dash at end of character class to avoid range error in GNU sed
grep -v '^[[:space:]]*#' "$f" | \
grep -qE '[+[:space:]=-](crc32b|crc32|tiger|haval|whirlpool|md5|sha1|rmd160|gost)([+[:space:]-]|$)' && return 0
# standalone S attribute in an expression (skip comment lines)
grep -v '^[[:space:]]*#' "$f" | grep -qE '(^|[+=[:space:]-])S([+[:space:]-]|$)' && return 0
# deprecated preprocessor macros
grep -qE '^[[:space:]]*@@(ifdef|ifndef|ifhost|ifnhost)[[:space:]]' "$f" && return 0
# missing trailing newline
[[ -s "$f" ]] && [[ "$(tail -c1 "$f" | wc -l)" -eq 0 ]] && return 0
return 1
}
# remove_hashsum FILE HASHSUM
# Remove a single hashsum token from all group/rule expressions in FILE (in-place).
# Handles all positions: +hash, -hash, hash+ (first), and hash$ (last with minus prefix).
remove_hashsum() {
local file="$1" hash="$2"
# Pass 1: remove hash when it is preceded by an operator (+ or -)
# Keep whatever follows (next operator, space, or end-of-line).
sed -E -i \
-e "/^[[:space:]]*#/!s/[+-]${hash}([+[:space:]-])/\1/g" \
-e "/^[[:space:]]*#/!s/[+-]${hash}$//" \
"$file"
# Pass 2: remove hash at the start of an expression (after = or whitespace),
# not preceded by an operator (those were handled in pass 1).
sed -E -i \
-e "/^[[:space:]]*#/!s/(=[[:space:]]*)${hash}([+[:space:]-])/\1\2/g" \
-e "/^[[:space:]]*#/!s/(=[[:space:]]*)${hash}$/\1/" \
-e "/^[[:space:]]*#/!s/([[:space:]])${hash}([+[:space:]-])/\1\2/g" \
-e "/^[[:space:]]*#/!s/([[:space:]])${hash}$/\1/" \
"$file"
# Clean up artifacts: double operators, dangling operator after '=', trailing operator.
sed -E -i \
-e '/^[[:space:]]*#/!s/[+][+]/+/g' \
-e '/^[[:space:]]*#/!s/[+][-]/+/g' \
-e '/^[[:space:]]*#/!s/[-][+]/-/g' \
-e '/^[[:space:]]*#/!s/(=[[:space:]]*)[+-]/\1/g' \
-e '/^[[:space:]]*#/!s/[+-]$//' \
"$file"
}
# migrate_config_file FILE
# Apply all config transformations to FILE. Sets global REINIT_NEEDED when needed.
migrate_config_file() {
local file="$1"
if [[ ! -f "$file" ]]; then
info "WARNING: $file not found, skipping"
return 0
fi
if [[ ! -r "$file" ]]; then
info "WARNING: $file is not readable, skipping"
return 0
fi
if ! needs_migration "$file"; then
info "no migration needed: $file"
return 0
fi
info "migrating: $file"
backup_file "$file"
local tmpfile
tmpfile="$(mktemp "${file}.XXXXXX")"
# Ensure tmpfile is cleaned up if we exit abnormally before the final mv
trap "rm -f '$tmpfile'; restore_backups" ERR
cp -p -- "$file" "$tmpfile"
# -----------------------------------------------------------------------
# Rename removed config options
# The 'database' key anchors to KEY= so it cannot match 'database_in'/'database_out'.
# -----------------------------------------------------------------------
config_has "$tmpfile" database && log_action "rename: database= → database_in="
config_has "$tmpfile" grouped && log_action "rename: grouped= → report_grouped="
config_has "$tmpfile" summarize_changes && log_action "rename: summarize_changes= → report_summarize_changes="
config_has "$tmpfile" ignore_list && log_action "rename: ignore_list= → report_ignore_changed_attrs="
config_has "$tmpfile" report_attributes && log_action "rename: report_attributes= → report_force_attrs="
if ! $DRY_RUN; then
config_rename "$tmpfile" database database_in
config_rename "$tmpfile" grouped report_grouped
config_rename "$tmpfile" summarize_changes report_summarize_changes
config_rename "$tmpfile" ignore_list report_ignore_changed_attrs
config_rename "$tmpfile" report_attributes report_force_attrs
fi
# -----------------------------------------------------------------------
# Replace verbose= with the equivalent 0.19 options
# -----------------------------------------------------------------------
if config_has "$tmpfile" verbose; then
log_action "remove verbose="
config_has "$tmpfile" log_level || log_action "add log_level=warning"
config_has "$tmpfile" report_level || log_action "add report_level=changed_attributes"
if ! $DRY_RUN; then
config_del "$tmpfile" verbose
append_setting "$tmpfile" 'log_level=warning'
append_setting "$tmpfile" 'report_level=changed_attributes'
fi
fi
# -----------------------------------------------------------------------
# Remove sql: database URL lines (PostgreSQL backend removed)
# Match lines whose value (after =) begins with sql:.
# -----------------------------------------------------------------------
if grep -qE '^[[:space:]]*[a-z_]+[[:space:]]*=[[:space:]]*sql:' "$tmpfile"; then
log_action "remove sql: database URL lines"
if $DRY_RUN; then
# Check which keys survive after sql: removal (i.e. have at least one non-sql: value)
grep -E '^[[:space:]]*database_out[[:space:]]*=' "$tmpfile" | \
grep -qvE '^[[:space:]]*[a-z_]+[[:space:]]*=[[:space:]]*sql:' || \
info " would add default database_out=file:/var/lib/aide/aide.db.new.gz"
grep -E '^[[:space:]]*database_in[[:space:]]*=' "$tmpfile" | \
grep -qvE '^[[:space:]]*[a-z_]+[[:space:]]*=[[:space:]]*sql:' || \
info " would add default database_in=file:/var/lib/aide/aide.db.gz"
fi
if ! $DRY_RUN; then
sed -E -i '/^[[:space:]]*[a-z_]+[[:space:]]*=[[:space:]]*sql:/d' "$tmpfile"
config_has "$tmpfile" database_out || {
append_setting "$tmpfile" 'database_out=file:/var/lib/aide/aide.db.new.gz'
info " WARNING: sql: URL removed; default database_out added." \
"Verify storage path before running aide --init."
}
config_has "$tmpfile" database_in || {
append_setting "$tmpfile" 'database_in=file:/var/lib/aide/aide.db.gz'
info " WARNING: sql: database_in removed; default database_in added." \
"Verify path before running aide --init."
}
fi
REINIT_NEEDED=true
fi
# -----------------------------------------------------------------------
# Remove 'h' from report_ignore_e2fsattrs
# Use the sed address form to remove ALL 'h' characters from the value line.
# -----------------------------------------------------------------------
if grep -qE '^[[:space:]]*report_ignore_e2fsattrs[[:space:]]*=.*h' "$tmpfile"; then
log_action "remove 'h' from report_ignore_e2fsattrs"
if ! $DRY_RUN; then
sed -E -i '/^[[:space:]]*report_ignore_e2fsattrs[[:space:]]*=/s/h//g' "$tmpfile"
sed -E -i '/^[[:space:]]*report_ignore_e2fsattrs[[:space:]]*=[[:space:]]*$/d' "$tmpfile"
fi
fi
# -----------------------------------------------------------------------
# Remove deprecated and removed hashsums
# Process crc32b before crc32 to avoid prefix collision.
# Character classes use dash at end to prevent GNU sed range-error.
# -----------------------------------------------------------------------
local hash changed_hashes=false
for hash in crc32b crc32 tiger haval whirlpool md5 sha1 rmd160 gost; do
if grep -v '^[[:space:]]*#' "$tmpfile" | grep -qE "(^|[+[:space:]=-])${hash}([+[:space:]-]|\$)"; then
log_action "remove hashsum: $hash"
if ! $DRY_RUN; then
remove_hashsum "$tmpfile" "$hash"
fi
changed_hashes=true
REINIT_NEEDED=true
fi
done
# Post-removal: fill any group definition whose RHS became empty with sha256
if $changed_hashes; then
if ! $DRY_RUN; then
while IFS= read -r lineno; do
[[ -z "$lineno" ]] && continue
sed -i "${lineno}s/=.*/= sha256/" "$tmpfile"
info " group on line $lineno became empty after hashsum removal; added sha256"
done < <(grep -nE '^[A-Za-z0-9]+[[:space:]]*=[[:space:]]*[+-]?[[:space:]]*$' \
"$tmpfile" | cut -d: -f1)
else
info " note: any group containing only deprecated hashsums will have sha256 substituted"
fi
fi
# -----------------------------------------------------------------------
# Replace deprecated S attribute with growing+s
# Applies since AIDE 0.16 is being replaced; growing+s is unknown to 0.16.
# Character classes use dash at end to prevent GNU sed range-error.
# -----------------------------------------------------------------------
if grep -qE '(^|[+=[:space:]-])S([+[:space:]-]|$)' "$tmpfile"; then
log_action "replace S attribute with growing+s"
if ! $DRY_RUN; then
sed -E -i \
-e '/^[[:space:]]*#/!s/([+=[:space:]-])S([+[:space:]-]|$)/\1growing+s\2/g' \
-e '/^[[:space:]]*#/!s/^S([+[:space:]-]|$)/growing+s\1/g' \
"$tmpfile"
fi
fi
# -----------------------------------------------------------------------
# Replace deprecated @@ifdef/@@ifndef/@@ifhost/@@ifnhost macros
# -----------------------------------------------------------------------
if grep -qE '^[[:space:]]*@@(ifdef|ifndef|ifhost|ifnhost)[[:space:]]' "$tmpfile"; then
log_action "replace deprecated @@ifdef/@@ifndef/@@ifhost/@@ifnhost macros"
if ! $DRY_RUN; then
sed -E -i \
-e 's/^([[:space:]]*)@@ifdef([[:space:]])/\1@@if defined\2/g' \
-e 's/^([[:space:]]*)@@ifndef([[:space:]])/\1@@if not defined\2/g' \
-e 's/^([[:space:]]*)@@ifhost([[:space:]])/\1@@if hostname\2/g' \
-e 's/^([[:space:]]*)@@ifnhost([[:space:]])/\1@@if not hostname\2/g' \
"$tmpfile"
fi
fi
# -----------------------------------------------------------------------
# Ensure file ends with a newline
# -----------------------------------------------------------------------
local last_byte
last_byte="$(tail -c1 "$tmpfile" | od -An -tx1 | tr -d ' \n')"
if [[ -n "$last_byte" && "$last_byte" != '0a' ]]; then
log_action "add missing trailing newline"
$DRY_RUN || echo "" >> "$tmpfile"
fi
# H group's content changed in 0.19; warn if used without a custom definition.
# Only reached when the file had real 0.16-style options, so this never fires on a
# clean 0.19 config.
if grep -qE '(^|[+[:space:]-])H([+[:space:]-]|$)' "$tmpfile" 2>/dev/null; then
if ! grep -qE '^[[:space:]]*H[[:space:]]*=' "$tmpfile"; then
info "NOTE: built-in H group in use without custom definition;" \
"H content changed in 0.19 — run 'aide --init' to rebuild the database"
fi
fi
# -----------------------------------------------------------------------
# Commit changes
# -----------------------------------------------------------------------
if ! $DRY_RUN; then
mv -- "$tmpfile" "$file"
# Restore original permissions
chmod --reference="${BACKUP_COPY[-1]}" "$file" 2>/dev/null || true
else
rm -f "$tmpfile"
fi
# Disarm the local ERR trap and re-arm the global one
trap - ERR
trap restore_backups ERR
}
# ---------------------------------------------------------------------------
# Post-migration warnings
# ---------------------------------------------------------------------------
check_and_warn() {
local file="$1"
# Warn if a rule path starts with a macro variable.
# Only flag lines where @@{...} appears at the very start (after optional whitespace);
# mid-path macros like /path/@@{VAR}/sub are valid and must not be flagged.
local macro_rules
macro_rules="$(grep -nE '^[[:space:]]*@@\{[^}]+\}' "$file" 2>/dev/null || true)"
if [[ -n "$macro_rules" ]]; then
info "WARNING ($file): the following rule paths start with a macro variable." \
"Rewrite them so the path begins with a literal '/':"
echo "$macro_rules" >&2
fi
# Warn if a group name contains non-alphanumeric characters.
# Require an uppercase first letter to avoid false positives on config option names.
# Underscores are accepted by AIDE 0.19.2; only '-' and '.' cause parse errors.
local bad_groups
bad_groups="$(grep -nE '^[A-Z][A-Za-z0-9]*[-.][A-Za-z0-9][^=]*[[:space:]]*=' \
"$file" 2>/dev/null || true)"
if [[ -n "$bad_groups" ]]; then
info "WARNING ($file): the following group names contain non-alphanumeric characters." \
"Rename groups and all their references to [A-Za-z0-9] only:"
echo "$bad_groups" >&2
fi
}
# ---------------------------------------------------------------------------
# Database reinitialisation
# ---------------------------------------------------------------------------
# expand_aide_macros CONFIG VALUE
# Expand @@{NAME} references in VALUE using @@define lines from CONFIG.
expand_aide_macros() {
local config="$1" value="$2"
local key val
while IFS= read -r defline; do
key="$(echo "$defline" | awk '{print $2}')"
val="$(echo "$defline" | awk '{$1=$2=""; print substr($0,3)}')"
value="${value//@@\{${key}\}/$val}"
done < <(grep -E '^[[:space:]]*@@define[[:space:]]' "$config" || true)
echo "$value"
}
# reinit_database CONFIG_FILE
# Backup the existing database, run aide --init, move the new DB into place.
reinit_database() {
local config="$1"
# Parse database_in path (file: URLs only)
local raw_in
raw_in="$(grep -E '^[[:space:]]*database_in[[:space:]]*=' "$config" | \
head -1 | sed -E 's/^[^=]+=file://')" || true
[[ -z "$raw_in" ]] && { info "WARNING: database_in not found in config; skipping reinit"; return 0; }
local db_in
db_in="$(expand_aide_macros "$config" "$raw_in")"
# Parse database_out path (file: URLs only)
local raw_out
raw_out="$(grep -E '^[[:space:]]*database_out[[:space:]]*=' "$config" | \
head -1 | sed -E 's/^[^=]+=file://')" || true
[[ -z "$raw_out" ]] && { info "WARNING: database_out not found in config; skipping reinit"; return 0; }
local db_out
db_out="$(expand_aide_macros "$config" "$raw_out")"
if [[ ! -f "$db_in" ]]; then
info "no existing database at $db_in; run 'aide --init -c $config' when ready"
return 0
fi
info "reinitialising database (this may take several minutes)..."
backup_file "$db_in"
aide --init -c "$config"
if [[ ! -f "$db_out" ]]; then
die "aide --init completed but output database not found at $db_out"
fi
mv -- "$db_out" "$db_in"
chmod 0600 "$db_in"
chown root:root "$db_in"
info "database reinitialised: $db_in"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
# CLI flags are out of scope for this script
info "Note: This script migrates aide config files only." \
"If you use '--verbose' or '--report' flags in wrapper scripts," \
"cron jobs, or systemd units, remove those flags manually."
# Argument parsing
local config_file=""
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--skip-init) SKIP_INIT=true; shift ;;
-h|--help) usage ;;
--) shift; break ;;
-*) die "unknown option: $1" ;;
*) config_file="$1"; shift ;;
esac
done
[[ $# -gt 0 ]] && die "unexpected argument: $1"
[[ -z "$config_file" ]] && usage
[[ -f "$config_file" ]] || die "config file not found: $config_file"
[[ -r "$config_file" ]] || die "config file not readable: $config_file"
$DRY_RUN || [[ -w "$config_file" ]] || die "config file not writable: $config_file"
# Verify aide >= 0.19 is installed
local aide_ver
aide_ver="$(aide --version 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1)" || true
if [[ -z "$aide_ver" ]]; then
info "aide not found; skipping migration"
exit 0
fi
local aide_major aide_minor
aide_major="${aide_ver%%.*}"
aide_minor="${aide_ver#*.}"
if [[ "$aide_major" -lt 1 && "$aide_minor" -lt 19 ]]; then
info "aide $aide_ver < 0.19; skipping migration"
exit 0
fi
$DRY_RUN && info "DRY-RUN mode: no files will be modified"
# Global ERR trap for cleanup
trap restore_backups ERR
# Collect all config files to process
local -a config_files=("$config_file")
while IFS= read -r inc; do
[[ -f "$inc" ]] && config_files+=("$inc")
done < <(extract_includes "$config_file")
# Migrate each config file
local f
for f in "${config_files[@]}"; do
migrate_config_file "$f"
done
# Post-migration warnings
for f in "${config_files[@]}"; do
check_and_warn "$f"
done
# Validate the resulting config
if ! $DRY_RUN; then
if ! aide --config-check -c "$config_file" >/dev/null 2>&1; then
info "ERROR: aide --config-check failed after migration; restoring all backups"
restore_backups
exit 1
fi
info "aide --config-check passed"
fi
# Database reinit
if $REINIT_NEEDED && ! $SKIP_INIT && ! $DRY_RUN; then
reinit_database "$config_file"
elif $REINIT_NEEDED && $SKIP_INIT && ! $DRY_RUN; then
info "database reinitialisation required but --skip-init set;" \
"run 'aide --init -c $config_file' manually"
elif $REINIT_NEEDED && $DRY_RUN; then
info "would require database reinitialisation after migration"
fi
info "migration complete"
}
main "$@"

View File

@ -23,8 +23,8 @@ database_add_metadata=yes
# Warn about unrestricted rules during config check (default: false)
config_check_warn_unrestricted_rules=false
# Number of workers for parallel processing (default: 1, can use percentage)
num_workers=1
# Number of workers for parallel processing (AIDE default: 1, overridden to 4 here)
num_workers=4
# Default.
log_level=warning
@ -80,20 +80,6 @@ report_url=stdout
#stribog256: GOST R 34.11-2012, 256 bit
#stribog512: GOST R 34.11-2012, 512 bit
# DEPRECATED (will be removed in future versions):
#md5: md5 checksum (deprecated since v0.19)
#sha1: sha1 checksum (deprecated since v0.19)
#rmd160: rmd160 checksum (deprecated since v0.19)
#gost: gost checksum (deprecated since v0.19)
# REMOVED in AIDE v0.19:
#S: check for growing size (use 'growing+s' instead)
#tiger: tiger checksum (removed)
#haval: haval checksum (removed)
#crc32: crc32 checksum (removed)
#crc32b: crc32b checksum (removed)
#whirlpool: whirlpool checksum (removed)
# Special attributes for advanced use cases:
#I: ignore changed filename - detects moved files by inode
#growing: ignore growing file size/timestamps for logs
@ -138,6 +124,10 @@ LOG = p+ftype+u+g+n+ANF+ARF+selinux+xattrs
# but we want to know when the data inside them changes - updated with modern hash
DATAONLY = ftype+p+l+n+u+g+s+acl+selinux+xattrs+sha256
# Read /etc/aide.d/*.conf files
@@include /etc/aide.d ^[a-zA-Z0-9_-]+\.conf$
# Next decide what directories/files you want in the database.
/boot NORMAL

View File

@ -1,7 +1,7 @@
Summary: Intrusion detection environment
Name: aide
Version: 0.19.2
Release: 5%{?dist}
Release: 7%{?dist}
URL: https://github.com/aide/aide
License: GPLv2+
@ -15,6 +15,11 @@ Source3: aide.conf
Source4: README.quickstart
Source5: aide.logrotate
Source6: aide-tmpfiles.conf
Source7: aide-migrate-config
Source8: aide-check.service
Source9: aide-check.timer
Patch0: aide-0.19.2-syslog-format.patch
Patch1: aide-include-permission-checks.patch
BuildRequires: gcc
BuildRequires: make
@ -66,21 +71,57 @@ mkdir -p %{buildroot}%{_localstatedir}/log/aide
mkdir -p -m0700 %{buildroot}%{_localstatedir}/lib/aide
# Install tmpfiles config
install -Dpm0644 %{SOURCE6} %{buildroot}%{_tmpfilesdir}/aide.conf
install -Dpm0755 %{SOURCE7} %{buildroot}%{_sbindir}/aide-migrate-config
# Create /etc/aide.d/
mkdir -p -m0700 %{buildroot}%{_sysconfdir}/aide.d
# Install systemd timer and service for scheduled integrity checks
install -Dpm0644 %{SOURCE8} %{buildroot}%{_unitdir}/aide-check.service
install -Dpm0644 %{SOURCE9} %{buildroot}%{_unitdir}/aide-check.timer
%files
%license COPYING
%doc AUTHORS ChangeLog NEWS README
%doc README.quickstart
%{_sbindir}/aide
%{_sbindir}/aide-migrate-config
%{_mandir}/man1/*.1*
%{_mandir}/man5/*.5*
%config(noreplace) %attr(0600,root,root) %{_sysconfdir}/aide.conf
%config(noreplace) %{_sysconfdir}/logrotate.d/aide
%dir %attr(0700,root,root) %{_sysconfdir}/aide.d
%dir %attr(0700,root,root) %{_localstatedir}/lib/aide
%dir %attr(0700,root,root) %{_localstatedir}/log/aide
%{_tmpfilesdir}/aide.conf
%{_unitdir}/aide-check.service
%{_unitdir}/aide-check.timer
%post
%systemd_post aide-check.timer
if [ $1 -ge 2 ]; then
/usr/sbin/aide-migrate-config /etc/aide.conf 2>&1 | \
tee -a /var/log/aide/aide-migrate.log || :
fi
%preun
%systemd_preun aide-check.timer
%postun
%systemd_postun_with_restart aide-check.timer
%changelog
* Tue Jun 03 2026 Patrik Koncity <pkoncity@redhat.com> - 0.19.2-7
- Support for included files in /etc/aide.d/
Resolves: RHEL-178122
- Increase default values for num_workers
Resolves: RHEL-178123
- Add pre-configured systemd timer for aide check
Resolves: RHEL-178121
* Tue May 26 2026 Attila Lakatos <alakatos@redhat.com> - 0.19.2-6
- Add aide-migrate-config to automate config migration from pre-0.19 syntax
Resolves: RHEL-178317
- Re-add syslog_format config option dropped during rebase to 0.19.2
* Wed Oct 15 2025 Attila Lakatos <alakatos@redhat.com> - 0.19.2-5
- Adjust default config to avoid false positives in /etc
Resolves: RHEL-83776