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}