    001/*
    002 * Copyright 2025 The Apache Software Foundation.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *      http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    013 * See the License for the specific language governing permissions and
    014 * limitations under the License.
    015 */
    016package org.apache.wiki.security;
    017
    018import com.google.gson.Gson;
    019import jakarta.mail.MessagingException;
    020import java.io.File;
    021import java.util.Date;
    022import java.util.Locale;
    023import java.util.Timer;
    024import java.util.TimerTask;
    025import java.util.concurrent.LinkedBlockingDeque;
    026import java.util.concurrent.ThreadFactory;
    027import java.util.concurrent.ThreadPoolExecutor;
    028import java.util.concurrent.TimeUnit;
    029import java.util.logging.Level;
    030import org.apache.log4j.Logger;
    031import org.apache.wiki.WikiEngine;
    032import org.apache.wiki.event.WikiEvent;
    033import org.apache.wiki.event.WikiEventListener;
    034import org.apache.wiki.event.WikiEventManager;
    035import org.apache.wiki.event.WikiSecurityEvent;
    036import org.apache.wiki.i18n.InternationalizationManager;
    037import org.apache.wiki.util.MailUtil;
    038
    039/**
    040 * Audit logger - listens to WikiEvent, logging specific events and actions that
    041 * audit worthy
    042 *
    043 * @since 3.0.0
    044 */
    045public final class AuditLogger implements WikiEventListener {
    046
    047    private static final Logger LOG = Logger.getLogger(AuditLogger.class);
    048
    049    private static AuditLogger INSTANCE;
    050
    051    public static void initialize(WikiEngine engine) {
    052
    053        if ("true".equals(engine.getWikiProperties().get("audit.enabled"))) {
    054            INSTANCE = new AuditLogger(true);
    055            INSTANCE.engine = engine;
    056            String minuteCheck = engine.getWikiProperties().getProperty("audit.alert.lowDiskSpaceFrequency", "30");
    057            INSTANCE.timer.scheduleAtFixedRate(new DiskSpaceCheck(), 0, Integer.parseInt(minuteCheck) * 60 * 1000L);
    058        } else {
    059            INSTANCE = new AuditLogger(false);
    060        }
    061    }
    062
    063    public static AuditLogger getInstance() {
    064        return INSTANCE;
    065    }
    066    private Timer timer;
    067    private WikiEngine engine;
    068    private final Gson gson = new Gson();
    069    private ThreadPoolExecutor threadPool = null;
    070
    071    private AuditLogger(boolean enabled) {
    072        //listen to all events
    073        if (enabled) {
    074            WikiEventManager.addWikiEventListener(WikiEventManager.class, this);
    075            timer = new Timer("AuditLogTasks", true);
    076            threadPool = new ThreadPoolExecutor(1, Runtime.getRuntime().availableProcessors(),
    077                    30, TimeUnit.SECONDS, new LinkedBlockingDeque<>(100), new ThreadFactory() {
    078                @Override
    079                public Thread newThread(Runnable r) {
    080                    Thread t = new Thread(r);
    081                    t.setName("Audit Log Email alert worker");
    082                    t.setDaemon(true);
    083                    return t;
    084                }
    085            }
    086            );
    087        } else {
    088            LOG.info("Audit logging is disabled, as well as low disk space monitoring");
    089        }
    090    }
    091
    092    public void shutdown() {
    093        engine = null;
    094        if (timer != null) {
    095            timer.cancel();
    096        }
    097        if (threadPool != null) {
    098            threadPool.shutdown();
    099        }
    100    }
    101
    102    @Override
    103    public void actionPerformed(WikiEvent event) {
    104        try {
    105            LOG.info(String.format(
    106                    "Class=%s, Description=%s, At=%d, AsString=%s, Name=%s, HttpsBits=%s",
    107                    event.getClass().getSimpleName(),
    108                    event.getTypeDescription(),
    109                    event.getWhen(),
    110                    event.toString(),
    111                    event.eventName(),
    112                    gson.toJson(event.getAttributes())));
    113            if (event instanceof WikiSecurityEvent wse) {
    114                String filters = engine.getWikiProperties().getProperty("audit.alert.filter", "41,42,43,46,47,52");
    115                String[] alertsWeCareAbout = filters.split("\\,");
    116                boolean keep = false;
    117                for (String s : alertsWeCareAbout) {
    118                    if (s.equals(wse.getType() + "")) {
    119                        keep = true;
    120                        break;
    121                    }
    122                }
    123                if (!keep) {
    124                    //not on the list of alerts the system owner wants emails on.
    125                    //so we stop processing here
    126                    return;
    127                }
    128                final Locale m_loc = Locale.getDefault();
    129                //TODO maybe we can look up the admin's account and get their
    130                //desired locale at some point
    131                final InternationalizationManager i18n = engine.getManager(InternationalizationManager.class);
    132                final String app = engine.getApplicationName();
    133                final String destinations = engine.getWikiProperties().getProperty("audit.alert.to");
    134                if (destinations == null) {
    135                    return;
    136                }
    137                final String[] addrs = destinations.split("\\;");
    138                final String subject = i18n.get(InternationalizationManager.DEF_TEMPLATE, m_loc,
    139                        "notification.auditlog.subject", app);
    140                /*
    141                Class: {0}\n\
    142                Event Name: {1}\n\
    143                Type Description: {2}\n\
    144                When: {3}\n\
    145                Event Details: {4}\n\
    146                Headers and Connection Details: {5}
    147                 */
    148                final String content = i18n.get(InternationalizationManager.DEF_TEMPLATE, m_loc,
    149                        "notification.auditlog.content",
    150                        event.getClass().getSimpleName(),
    151                        event.eventName(),
    152                        event.getTypeDescription(),
    153                        new Date(event.getWhen()).toString(),
    154                        event.toString(),
    155                        gson.toJson(event.getAttributes()));
    156                for (String to : addrs) {
    157                    threadPool.submit(() -> {
    158                        try {
    159                            MailUtil.sendMessage(engine.getWikiProperties(),
    160                                    to, subject, content);
    161                        } catch (Exception ex) {
    162                            LOG.warn("Audit alert email to " + to + " failed with " + ex.getMessage());
    163                            if (LOG.isDebugEnabled()) {
    164                                LOG.debug("Audit alert email to " + to + " failed with " + ex.getMessage(), ex);
    165                            }
    166                        }
    167                    });
    168
    169                }
    170            }
    171
    172        } catch (Exception ex) {
    173            LOG.error("Failed to log audit event " + ex.getMessage(), ex);
    174        }
    175    }
    176
    177    private static class DiskSpaceCheck extends TimerTask {
    178
    179        @Override
    180        public void run() {
    181            double threshold = Double.parseDouble(INSTANCE.engine.getWikiProperties().getProperty("audit.alert.lowDiskSpaceThreshold", "75"));
    182            File f = new File(".");
    183            long free = f.getFreeSpace();
    184            long total = f.getTotalSpace();
    185            long used = total - free;
    186            if (((double) used / (double) total * 100d) >= threshold) {
    187                WikiSecurityEvent wse = new WikiSecurityEvent(this, WikiSecurityEvent.LOW_STORAGE, null, this);
    188                org.apache.wiki.event.WikiEventManager.fireEvent(this, wse);
    189            }
    190        }
    191
    192    }
    193
    194}