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}