/*
* Copyright 2011 NCHOVY
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.krakenapps.siem.analyzer;
import java.net.InetAddress;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Invalidate;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.apache.felix.ipojo.annotations.Validate;
import org.krakenapps.event.api.Event;
import org.krakenapps.event.api.EventDispatcher;
import org.krakenapps.event.api.EventProvider;
import org.krakenapps.event.api.EventSeverity;
import org.krakenapps.siem.LogServer;
import org.krakenapps.siem.NormalizedLog;
import org.krakenapps.siem.NormalizedLogListener;
@Component(name = "siem-login-failure-analyzer")
@Provides
public class LoginFailureAnalyzerEngine implements NormalizedLogListener, LoginFailureAnalyzer, EventProvider {
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(LoginFailureAnalyzer.class.getName());
@Requires
private LogServer logServer;
@Requires
private EventDispatcher eventDispatcher;
private Set<LoginFailureEventListener> callbacks;
private static ConcurrentMap<InetAddress, FailureStat> m = new ConcurrentHashMap<InetAddress, FailureStat>();
private WallClock clock;
// milliseconds
private long idleTime;
private long ignoreTime;
private long eventPeriod;
private int limitCount;
/**
* EventProvider.getName()
*/
@Override
public String getName() {
return "login-bruteforce";
}
public LoginFailureAnalyzerEngine() {
this.limitCount = 3;
this.idleTime = 10000;
this.ignoreTime = 5000;
this.eventPeriod = 3600000;
this.clock = new SystemWallClock();
callbacks = Collections.newSetFromMap(new ConcurrentHashMap<LoginFailureEventListener, Boolean>());
}
@Validate
public void start() {
logServer.addNormalizedLogListener("login", this);
}
@Invalidate
public void stop() {
if (logServer != null)
logServer.removeNormalizedLogListener("login", this);
}
@Override
public void onLog(NormalizedLog log) {
Date date = (Date) log.get("date");
InetAddress ip = (InetAddress) log.get("src_ip");
String result = log.getString("result");
if (date == null || ip == null || result == null || result.equals("success"))
return;
if (logger.isDebugEnabled())
logger.debug("kraken siem: received login log, ip [{}], access [{}]", ip, date);
FailureStat fs = new FailureStat(ip, new AtomicInteger(1), date);
FailureStat old = m.putIfAbsent(ip, fs);
if (old != null)
fs = old;
long interval = getIntervalTime(date, fs.lastSeen);
if (interval >= idleTime) {
logger.debug("kraken siem: login failure interval {} reset {}", interval, ip);
fs.firstSeen = date;
fs.lastSeen = date;
fs.count.set(0);
fs.alerted = false;
} else if (fs.count.get() >= limitCount) {
fs.lastSeen = date;
if (!fs.alerted || !isIgnored(fs.firstSeen)) {
fs.alerted = true;
if (logger.isTraceEnabled())
traceAlert(ip, fs);
if (fs.lastDispatch == null || clock.now().getTime() - fs.lastDispatch.getTime() >= eventPeriod) {
generateEvent(fs.firstSeen, fs.lastSeen, fs.ip, fs.count.get());
invokeCallbacks(ip, fs);
fs.lastDispatch = clock.now();
}
}
}
fs.count.getAndIncrement();
m.put(ip, fs);
}
private void traceAlert(InetAddress ip, FailureStat fs) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
logger.trace(
"kraken siem: [{}] tries login bruteforce attack [{}] times, first seen [{}], last seen [{}]",
new Object[] { ip.getHostAddress(), fs.count.get(), dateFormat.format(fs.firstSeen),
dateFormat.format(fs.lastSeen) });
}
private void generateEvent(Date firstSeen, Date lastSeen, InetAddress source, int count) {
// TODO: destination ip setup
Event event = new Event();
event.setCategory("Attack");
event.setFirstSeen(firstSeen);
event.setLastSeen(lastSeen);
event.setSeverity(EventSeverity.Critical);
event.setMessageKey("login-bruteforce");
event.setSourceIp(source);
event.setCount(count);
eventDispatcher.dispatch(event);
}
private void invokeCallbacks(InetAddress ip, FailureStat fs) {
for (LoginFailureEventListener callback : callbacks) {
try {
callback.onBruteforceAttack(ip, fs.count.get());
} catch (Exception e) {
logger.warn("kraken siem: login failure analyzer listener should not throw any exception", e);
}
}
}
public void setWallClock(WallClock clock) {
this.clock = clock;
}
static class FailureStat {
InetAddress ip;
AtomicInteger count;
Date firstSeen;
Date lastSeen;
Date lastDispatch;
boolean alerted;
public FailureStat(InetAddress ip, AtomicInteger count, Date firstSeen) {
this.ip = ip;
this.count = count;
this.firstSeen = firstSeen;
this.lastSeen = firstSeen;
this.lastDispatch = null;
}
}
public void register(LoginFailureEventListener event) {
callbacks.add(event);
}
public void unregister(LoginFailureEventListener event) {
callbacks.remove(event);
}
private boolean isIgnored(Date firstSeen) {
Date now = clock.now();
return now.getTime() - firstSeen.getTime() < ignoreTime;
}
private long getIntervalTime(Date recent, Date lastAccess) {
return recent.getTime() - lastAccess.getTime();
}
private static class SystemWallClock implements WallClock {
@Override
public Date now() {
return new Date();
}
}
}