From cadece63301dff197e790ebb9b9ad85e3b755b55 Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Mon, 24 Aug 2026 08:50:28 -0400 Subject: [PATCH] import CS aide-0.19.2-7.el9 --- SOURCES/README.quickstart | 41 +- SOURCES/aide-0.19.2-syslog-format.patch | 657 +++++++++++++++++++ SOURCES/aide-check.service | 10 + SOURCES/aide-check.timer | 11 + SOURCES/aide-include-permission-checks.patch | 66 ++ SOURCES/aide-migrate-config | 588 +++++++++++++++++ SOURCES/aide.conf | 22 +- SPECS/aide.spec | 43 +- 8 files changed, 1404 insertions(+), 34 deletions(-) create mode 100644 SOURCES/aide-0.19.2-syslog-format.patch create mode 100644 SOURCES/aide-check.service create mode 100644 SOURCES/aide-check.timer create mode 100644 SOURCES/aide-include-permission-checks.patch create mode 100755 SOURCES/aide-migrate-config diff --git a/SOURCES/README.quickstart b/SOURCES/README.quickstart index 87adc63..92e42dc 100644 --- a/SOURCES/README.quickstart +++ b/SOURCES/README.quickstart @@ -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. diff --git a/SOURCES/aide-0.19.2-syslog-format.patch b/SOURCES/aide-0.19.2-syslog-format.patch new file mode 100644 index 0000000..53f4ca7 --- /dev/null +++ b/SOURCES/aide-0.19.2-syslog-format.patch @@ -0,0 +1,657 @@ +From f3e62eb87e0a0e9c6fd43c933670447c8ab0517a Mon Sep 17 00:00:00 2001 +From: Cropi +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: 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 +--- + 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); + } ++"syslog_format" { ++ LOG_LEX_TOKEN(lex_log_level, CONFIGOPTION (SYSLOG_FORMAT_OPTION), conftext) ++ conflval.option = SYSLOG_FORMAT_OPTION; ++ BEGIN (STRINGEQHUNT); ++ return (CONFIGOPTION); ++} ++ + + "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 ++#include ++#include ++#include ++#include ++#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_=|[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_=A:|D: ++ * ++ * Both A and D are initialized to "" 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 = ""; ++ const char *D = ""; ++ 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 + diff --git a/SOURCES/aide-check.service b/SOURCES/aide-check.service new file mode 100644 index 0000000..8946759 --- /dev/null +++ b/SOURCES/aide-check.service @@ -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 diff --git a/SOURCES/aide-check.timer b/SOURCES/aide-check.timer new file mode 100644 index 0000000..5bbfa1e --- /dev/null +++ b/SOURCES/aide-check.timer @@ -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 diff --git a/SOURCES/aide-include-permission-checks.patch b/SOURCES/aide-include-permission-checks.patch new file mode 100644 index 0000000..012b640 --- /dev/null +++ b/SOURCES/aide-include-permission-checks.patch @@ -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); + } + } diff --git a/SOURCES/aide-migrate-config b/SOURCES/aide-migrate-config new file mode 100755 index 0000000..43a6b31 --- /dev/null +++ b/SOURCES/aide-migrate-config @@ -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] +# +# --dry-run Report what would change; do not modify any file. +# --skip-init Migrate config files only; do not reinitialise the database. +# 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] " >&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 "$@" diff --git a/SOURCES/aide.conf b/SOURCES/aide.conf index cb71b60..ee198b7 100644 --- a/SOURCES/aide.conf +++ b/SOURCES/aide.conf @@ -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 diff --git a/SPECS/aide.spec b/SPECS/aide.spec index 68b0865..d6eddb5 100644 --- a/SPECS/aide.spec +++ b/SPECS/aide.spec @@ -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 - 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 - 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 - 0.19.2-5 - Adjust default config to avoid false positives in /etc Resolves: RHEL-83776