Transform http and ssh logs into audit logs

Tranformation of SSH and HTTP logs into auditlogs will permit
to analyse audit log data even before the plugin was enabled.

Feature: Issue 10307
Change-Id: I404b17b4e18d6f0aa648bd13a471e1bf8847c04f
diff --git a/BUILD b/BUILD
index 2dec1e5..db99a0a 100644
--- a/BUILD
+++ b/BUILD
@@ -13,6 +13,7 @@
         "Gerrit-PluginName: audit-sl4j",
         "Gerrit-ReloadMode: reload",
         "Gerrit-Module: com.googlesource.gerrit.plugins.auditsl4j.Module",
+        "Gerrit-SshModule: com.googlesource.gerrit.plugins.auditsl4j.SshModule",
         "Implementation-Title: Gerrit Audit provider for SLF4J",
         "Implementation-URL: https://gerrit.googlesource.com/plugins/audit-sl4j/",
     ],
diff --git a/README.md b/README.md
index d4c4c19..7145605 100644
--- a/README.md
+++ b/README.md
@@ -103,3 +103,11 @@
   }
 }
 ```
+
+## Import old logs
+
+The makes available an ssh command to transform `http_logs` and `ssh_logs` into Audit Logs:
+
+```bash
+ssh -p 29418 admin@localhost audit-sl4j transform --from 2019-01-23 --until 2019-01-24
+```
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatRenderer.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatRenderer.java
index fca60b5..5627530 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatRenderer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditFormatRenderer.java
@@ -21,5 +21,7 @@
 
   String render(AuditEvent auditEvent);
 
+  String render(AuditEvent auditEvent, TransformableAuditLogType type);
+
   Optional<String> headers();
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
index a89be0a..451bfbd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRecord.java
@@ -27,4 +27,11 @@
     this.type = eventClass.substring(eventClass.lastIndexOf('.') + 1);
     this.event = event;
   }
+
+  public AuditRecord(AuditEvent event, TransformableAuditLogType type) {
+    super();
+
+    this.type = type.name();
+    this.event = event;
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
index 68eff99..106a02a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToCsv.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.audit.HttpAuditEvent;
 import com.google.gerrit.audit.RpcAuditEvent;
 import com.google.gerrit.audit.SshAuditEvent;
-
 import java.text.SimpleDateFormat;
 import java.util.Collection;
 import java.util.Collections;
@@ -32,7 +31,7 @@
 import java.util.TreeSet;
 
 public class AuditRendererToCsv implements AuditFormatRenderer {
-  
+
   private static final SimpleDateFormat dateFmt = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss.SSSS");
 
   @SuppressWarnings("serial")
@@ -47,18 +46,18 @@
               put(AuditEvent.class, new AuditEventFormat());
             }
           });
-  
+
   interface CsvFieldFormatter<T> {
     String formatToCsv(T result);
   }
-  
+
   static class RpcAuditEventFormat implements CsvFieldFormatter<RpcAuditEvent> {
     @Override
     public String formatToCsv(RpcAuditEvent result) {
       return "RPC-" + result.httpMethod + ", Status:" + result.httpStatus;
     }
   }
-  
+
   static class HttpAuditEventFormat implements CsvFieldFormatter<HttpAuditEvent> {
 
     @Override
@@ -66,14 +65,14 @@
       return "HTTP-" + result.httpMethod + ", Status:" + result.httpStatus;
     }
   }
-  
+
   static class SshAuditEventFormat implements CsvFieldFormatter<SshAuditEvent> {
     @Override
     public String formatToCsv(SshAuditEvent result) {
       return "SSH";
     }
   }
-  
+
   static class AuditEventFormat implements CsvFieldFormatter<SshAuditEvent> {
 
     @Override
@@ -99,6 +98,11 @@
   }
 
   @Override
+  public String render(AuditEvent auditEvent, TransformableAuditLogType type) {
+    return render(auditEvent);
+  }
+
+  @Override
   public Optional<String> headers() {
     return Optional.of(
         "EventId | EventTS | SessionId | User | Protocol data | Action | Parameters | Result | StartTS | Elapsed");
@@ -144,7 +148,6 @@
     }
     return out.toString();
   }
-  
 
   public static <T> String getFieldAsCsv(T result) {
     if (result == null) return "";
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java
index 400fd33..dbb9a61 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditRendererToJson.java
@@ -74,6 +74,11 @@
   }
 
   @Override
+  public String render(AuditEvent auditEvent, TransformableAuditLogType type) {
+    return gson.toJson(new AuditRecord(auditEvent, type));
+  }
+
+  @Override
   public Optional<String> headers() {
     return Optional.empty();
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditUser.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditUser.java
new file mode 100644
index 0000000..3ae8e62
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/AuditUser.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupMembership;
+
+public class AuditUser extends CurrentUser {
+  String username;
+
+  @Override
+  public GroupMembership getEffectiveGroups() {
+    return null;
+  }
+
+  @Override
+  public String getUserName() {
+    return username;
+  }
+
+  public void setUserName(String username) {
+    this.username = username;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
index 94bc55f..e62ee22 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/LoggerAudit.java
@@ -27,7 +27,7 @@
   public static final String AUDIT_LOGGER_NAME = LoggerAudit.class.getName();
 
   @Inject
-  LoggerAudit(AuditWriter auditWriter, AuditFormatRenderer auditRenderer) {
+  public LoggerAudit(AuditWriter auditWriter, AuditFormatRenderer auditRenderer) {
     this.auditWriter = auditWriter;
     this.auditRenderer = auditRenderer;
 
@@ -36,7 +36,15 @@
 
   @Override
   public void onAuditableAction(AuditEvent auditEvent) {
-    String auditString = auditRenderer.render(auditEvent);
+    String auditString = getAuditString(auditEvent);
     auditWriter.write(auditString);
   }
+
+  public String getAuditString(AuditEvent auditEvent) {
+    return auditRenderer.render(auditEvent);
+  }
+
+  public String getAuditString(AuditEvent auditEvent, TransformableAuditLogType type) {
+    return auditRenderer.render(auditEvent, type);
+  }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/SshModule.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/SshModule.java
new file mode 100644
index 0000000..125b2fc
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/SshModule.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.gerrit.sshd.PluginCommandModule;
+
+public class SshModule extends PluginCommandModule {
+  @Override
+  protected void configureCommands() {
+    command(TransformLogsCommand.class);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformLogsCommand.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformLogsCommand.java
new file mode 100644
index 0000000..6c5b463
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformLogsCommand.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.auditsl4j;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.auditsl4j.logsource.HTTPLog;
+import com.googlesource.gerrit.plugins.auditsl4j.logsource.SSHLog;
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.zip.GZIPInputStream;
+import org.kohsuke.args4j.Option;
+
+@CommandMetaData(name = "transform", description = "Transform ssh and http logs into audit logs")
+public class TransformLogsCommand extends SshCommand {
+
+  private LoggerAudit loggerAudit;
+  private final SitePaths sitePaths;
+
+  @Inject
+  public TransformLogsCommand(SitePaths sitePaths, LoggerAudit loggerAudit) {
+    this.sitePaths = sitePaths;
+    this.loggerAudit = loggerAudit;
+  }
+
+  SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+
+  @Option(name = "--from", usage = "transform logs from <YYYY-MM-DD>")
+  private String from;
+
+  @Option(name = "--until", usage = "transform logs until <YYYY-MM-DD>")
+  private String until;
+
+  @Override
+  public void run() {
+
+    Date dateFrom;
+    try {
+      dateFrom = format.parse(from);
+    } catch (Exception e) {
+      stderr.print("Invalid 'from' format: " + from + ", expected format <YYYY-MM-DD>");
+      return;
+    }
+    Date dateUntil;
+    try {
+      dateUntil = format.parse(until);
+    } catch (Exception e) {
+      stderr.print("Invalid 'until' format: " + until + ", expected format <YYYY-MM-DD>");
+      return;
+    }
+
+    if (dateFrom.after(dateUntil)) {
+      stderr.print("'from' cannot be after 'until'");
+      return;
+    }
+
+    Date currentDate = dateFrom;
+    while (currentDate.compareTo(dateUntil) <= 0) {
+      transformHttpdLogs(format.format(currentDate));
+      transformSshdLogs(format.format(currentDate));
+
+      currentDate = getTomorrowDate(currentDate);
+    }
+
+    stdout.print("Transformed HTTP and SSH logs from " + from + " until " + until + "!\n");
+  }
+
+  private Date getTomorrowDate(Date currentDate) {
+    Calendar c = Calendar.getInstance();
+    c.setTime(currentDate);
+    c.add(Calendar.DAY_OF_MONTH, 1);
+    return c.getTime();
+  }
+
+  private void transformSshdLogs(String currentDateString) {
+    transformLogs(currentDateString, SSHLog.logFilenameBase(), SSHLog::createFromLog);
+  }
+
+  private void transformHttpdLogs(String currentDateString) {
+    transformLogs(currentDateString, HTTPLog.logFilenameBase(), HTTPLog::createFromLog);
+  }
+
+  private void transformLogs(
+      String currentDateString,
+      String fileType,
+      Function<String, Optional<? extends TransformableLog>> createTransformable) {
+    // Log format example: httpd_log.2019-01-19.gz
+    String logFileName = sitePaths.logs_dir + "/" + fileType + "." + currentDateString + ".gz";
+    String auditLogFileName = sitePaths.logs_dir + "/audit_log." + currentDateString + ".log";
+
+    stdout.print("Transforming: " + logFileName + " => " + auditLogFileName + " ...\n");
+    stdout.flush();
+
+    try {
+      GZIPInputStream gzis = new GZIPInputStream(new FileInputStream(logFileName));
+      BufferedReader input = new BufferedReader(new InputStreamReader(gzis));
+
+      PrintWriter pw =
+          new PrintWriter(
+              Files.newBufferedWriter(
+                  Paths.get(auditLogFileName),
+                  StandardOpenOption.CREATE,
+                  StandardOpenOption.WRITE,
+                  StandardOpenOption.APPEND));
+
+      input
+          .lines()
+          .map(createTransformable)
+          .map(
+              maybeTransformableLog ->
+                  maybeTransformableLog.flatMap(
+                      transformableLog -> transformableLog.toAuditLog(loggerAudit)))
+          .filter(Optional::isPresent)
+          .map(Optional::get)
+          .forEach(pw::println);
+      pw.flush();
+    } catch (FileNotFoundException fnfe) {
+      stderr.print("Cannot find '" + logFileName + "'. Skipping!\n");
+    } catch (IOException e) {
+      stderr.print("Error: " + e.getMessage() + "!\n");
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformableAuditLogType.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformableAuditLogType.java
new file mode 100644
index 0000000..8a2b589
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformableAuditLogType.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.auditsl4j;
+
+public enum TransformableAuditLogType {
+  HttpAuditEvent,
+  ExtendedHttpAuditEvent,
+  SshAuditEvent
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformableLog.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformableLog.java
new file mode 100644
index 0000000..8c07866
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/TransformableLog.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// 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 com.googlesource.gerrit.plugins.auditsl4j;
+
+import java.util.Optional;
+
+public interface TransformableLog {
+  Optional<String> toAuditLog(LoggerAudit loggerAudit);
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/HTTPLog.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/HTTPLog.java
new file mode 100644
index 0000000..5de469f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/HTTPLog.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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 com.googlesource.gerrit.plugins.auditsl4j.logsource;
+
+import static com.google.gerrit.audit.AuditEvent.UNKNOWN_SESSION_ID;
+
+import com.google.gerrit.audit.HttpAuditEvent;
+import com.google.gerrit.server.AccessPath;
+import com.googlesource.gerrit.plugins.auditsl4j.AuditUser;
+import com.googlesource.gerrit.plugins.auditsl4j.LoggerAudit;
+import com.googlesource.gerrit.plugins.auditsl4j.TransformableAuditLogType;
+import com.googlesource.gerrit.plugins.auditsl4j.TransformableLog;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class HTTPLog implements TransformableLog {
+  private static final Logger log = LoggerFactory.getLogger(HTTPLog.class);
+  private String ip;
+  private String user;
+  private String method;
+  private String timestamp;
+  private String resource;
+  private String protocol;
+  private Integer status;
+  private String contentLength;
+  private String referrer;
+  private String userAgent;
+
+  public HTTPLog(
+      String ip,
+      String user,
+      String timestamp,
+      String method,
+      String resource,
+      String protocol,
+      Integer status,
+      String contentLength,
+      String referrer,
+      String userAgent) {
+    this.ip = ip;
+    this.user = user;
+    this.method = method;
+    this.timestamp = timestamp;
+    this.resource = resource;
+    this.protocol = protocol;
+    this.status = status;
+    this.contentLength = contentLength;
+    this.referrer = referrer;
+    this.userAgent = userAgent;
+  }
+
+  public static Optional<HTTPLog> createFromLog(String line) {
+    // HTTP log example:
+    // 104.32.164.100 - - [24/Jan/2019:00:00:03 +0000] "GET /plugins/events-log/ HTTP/1.1" 404 9 -
+    // "Apache-HttpClient/4.5.3 (Java/1.8.0_191)"
+    Matcher a =
+        Pattern.compile(
+                "^(?<ip>.*?)\\s-\\s(?<user>.*?)\\s\\["
+                    + "(?<timestamp>.*?)\\]\\s\""
+                    + "(?<method>\\w+)\\s(?<resource>.*?)\\s(?<protocol>.*?)\"\\s"
+                    + "(?<status>\\d+)\\s(?<contentLength>\\d+|-)\\s(?<referrer>.*?)\\s(?<userAgent>.*?)$")
+            .matcher(line);
+    if (a.matches()) {
+      try {
+        return Optional.of(
+            new HTTPLog(
+                a.group("ip"),
+                a.group("user"),
+                a.group("timestamp"),
+                a.group("method"),
+                a.group("resource"),
+                a.group("protocol"),
+                Integer.parseInt(a.group("status")),
+                a.group("contentLength"),
+                a.group("referrer"),
+                a.group("userAgent")));
+      } catch (Exception e) {
+        log.error("Something wrong while parsing line: " + line);
+      }
+    } else {
+      log.error("Can't extract any info from line: " + line);
+    }
+    return Optional.empty();
+  }
+
+  private AuditUser getAuditUser() {
+    AuditUser au = new AuditUser();
+    au.setUserName(this.user);
+    au.setAccessPath(getAccessPath());
+    return au;
+  }
+
+  private AccessPath getAccessPath() {
+    Matcher a = Pattern.compile("^\"git.*?").matcher(this.userAgent);
+    return a.matches() ? AccessPath.GIT : AccessPath.REST_API;
+  }
+
+  public Optional<Long> getWhen() {
+    SimpleDateFormat format = new SimpleDateFormat("dd/MMM/yyyy:hh:mm:ss Z");
+    try {
+      return Optional.of(format.parse(this.timestamp).getTime());
+    } catch (ParseException pe) {
+      log.error(
+          "Can't parse timestamp: '" + this.timestamp + "'. Error message: " + pe.getMessage());
+    }
+    return Optional.empty();
+  }
+
+  public Optional<String> toAuditLog(LoggerAudit loggerAudit) {
+    return getWhen()
+        .map(
+            when ->
+                new HttpAuditEvent(
+                    UNKNOWN_SESSION_ID,
+                    this.getAuditUser(),
+                    this.resource,
+                    when,
+                    null,
+                    this.method,
+                    null,
+                    this.status,
+                    null))
+        .map(
+            httpAuditEvent -> {
+              AuditUser au = getAuditUser();
+              if (au.getAccessPath() == AccessPath.REST_API) {
+                return loggerAudit.getAuditString(
+                    httpAuditEvent, TransformableAuditLogType.ExtendedHttpAuditEvent);
+              } else {
+                return loggerAudit.getAuditString(
+                    httpAuditEvent, TransformableAuditLogType.HttpAuditEvent);
+              }
+            });
+  }
+
+  public static String logFilenameBase() {
+    return "httpd_log";
+  }
+
+  public String getIp() {
+    return ip;
+  }
+
+  public String getUser() {
+    return user;
+  }
+
+  public String getMethod() {
+    return method;
+  }
+
+  public String getTimestamp() {
+    return timestamp;
+  }
+
+  public String getResource() {
+    return resource;
+  }
+
+  public String getProtocol() {
+    return protocol;
+  }
+
+  public Integer getStatus() {
+    return status;
+  }
+
+  public String getContentLength() {
+    return contentLength;
+  }
+
+  public String getReferrer() {
+    return referrer;
+  }
+
+  public String getUserAgent() {
+    return userAgent;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/SSHLog.java b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/SSHLog.java
new file mode 100644
index 0000000..c0f26f4
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/SSHLog.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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 com.googlesource.gerrit.plugins.auditsl4j.logsource;
+
+import com.google.gerrit.audit.SshAuditEvent;
+import com.google.gerrit.server.AccessPath;
+import com.googlesource.gerrit.plugins.auditsl4j.AuditUser;
+import com.googlesource.gerrit.plugins.auditsl4j.LoggerAudit;
+import com.googlesource.gerrit.plugins.auditsl4j.TransformableAuditLogType;
+import com.googlesource.gerrit.plugins.auditsl4j.TransformableLog;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SSHLog implements TransformableLog {
+  private static final Logger log = LoggerFactory.getLogger(SSHLog.class);
+
+  private String user;
+  private String session;
+  private String timestamp;
+  private String accountId;
+  private String command;
+  private String waitTime;
+  private String execTime;
+  private String status;
+
+  public SSHLog(
+      String timestamp,
+      String session,
+      String user,
+      String accountId,
+      String command,
+      String waitTime,
+      String execTime,
+      String status) {
+    this.user = user;
+    this.session = session;
+    this.timestamp = timestamp;
+    this.accountId = accountId;
+    this.command = command;
+    this.waitTime = waitTime;
+    this.execTime = execTime;
+    this.status = status;
+  }
+
+  public static Optional<SSHLog> createFromLog(String line) {
+    Matcher a =
+        Pattern.compile(
+                "^\\[(?<timestamp>.*?)\\]\\s(?<session>.*?)\\s"
+                    + "(?<user>.*?)\\s(?<accountId>.*?)\\s(?<command>.*?)(?:\\s"
+                    + "(?<waitTime>.*?)\\s(?<execTime>.*?)\\s(?<status>.*?))?$")
+            .matcher(line);
+
+    if (a.matches()) {
+      try {
+        return Optional.of(
+            new SSHLog(
+                a.group("timestamp"),
+                a.group("session"),
+                a.group("user"),
+                a.group("accountId"),
+                a.group("command"),
+                a.group("waitTime"),
+                a.group("execTime"),
+                a.group("status")));
+      } catch (Exception e) {
+        log.error("Something wrong while parsing line: " + line);
+      }
+    } else {
+      log.error("Can't extract any info from line: " + line);
+    }
+    return Optional.empty();
+  }
+
+  private AuditUser getAuditUser() {
+    AuditUser au = new AuditUser();
+    au.setUserName(this.user);
+    au.setAccessPath(AccessPath.SSH_COMMAND);
+    return au;
+  }
+
+  private Optional<Long> getWhen() {
+    // Timestamp format example: 2019-01-23 12:44:04,723 +0100
+    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS Z");
+    try {
+      return Optional.of(format.parse(this.timestamp).getTime());
+    } catch (ParseException pe) {
+      log.error(
+          "Can't parse timestamp: '" + this.timestamp + "'. Error message: " + pe.getMessage());
+    }
+    return Optional.empty();
+  }
+
+  public Optional<String> toAuditLog(LoggerAudit loggerAudit) {
+    return getWhen()
+        .map(
+            when -> new SshAuditEvent(this.session, getAuditUser(), this.command, when, null, null))
+        .map(
+            sshAuditEvent ->
+                loggerAudit.getAuditString(sshAuditEvent, TransformableAuditLogType.SshAuditEvent));
+  }
+
+  public static String logFilenameBase() {
+    return "sshd_log";
+  }
+
+  public String getUser() {
+    return user;
+  }
+
+  public String getSession() {
+    return session;
+  }
+
+  public String getTimestamp() {
+    return timestamp;
+  }
+
+  public String getAccountId() {
+    return accountId;
+  }
+
+  public String getCommand() {
+    return command;
+  }
+
+  public String getWaitTime() {
+    return waitTime;
+  }
+
+  public String getExecTime() {
+    return execTime;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/HttpLogTest.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/HttpLogTest.java
new file mode 100644
index 0000000..756c5cb
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/HttpLogTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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 com.googlesource.gerrit.plugins.auditsl4j.logsource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.googlesource.gerrit.plugins.auditsl4j.AuditRendererToJson;
+import com.googlesource.gerrit.plugins.auditsl4j.AuditWriterToStringList;
+import com.googlesource.gerrit.plugins.auditsl4j.LoggerAudit;
+import java.util.Optional;
+import org.junit.Test;
+
+public class HttpLogTest {
+
+  LoggerAudit loggerAudit =
+      new LoggerAudit(new AuditWriterToStringList(), new AuditRendererToJson());
+
+  @Test
+  public void succesullyParseHttpLog() {
+    String ip = "104.32.164.100";
+    String user = "myUser";
+    String method = "GET";
+    String timestamp = "24/Jan/2019:00:00:03 +0000";
+    String resource = "/plugins/events-log/";
+    String protocol = "HTTP/1.1";
+    Integer status = 404;
+    String contentLenght = "9";
+    String referrer = "ciccio";
+    String userAgent = "\"Apache-HttpClient/4.5.3 (Java/1.8.0_191)\"";
+    // 104.32.164.100 - - [24/Jan/2019:00:00:03 +0000] "GET /plugins/events-log/ HTTP/1.1" 404 9 -
+    // "Apache-HttpClient/4.5.3 (Java/1.8.0_191)"
+    String logLine =
+        String.format(
+            "%s - %s [%s] \"%s %s %s\" %d %s %s %s",
+            ip,
+            user,
+            timestamp,
+            method,
+            resource,
+            protocol,
+            status,
+            contentLenght,
+            referrer,
+            userAgent);
+
+    HTTPLog expected =
+        new HTTPLog(
+            ip,
+            user,
+            timestamp,
+            method,
+            resource,
+            protocol,
+            status,
+            contentLenght,
+            referrer,
+            userAgent);
+    Optional<HTTPLog> maybeHTTPLog = HTTPLog.createFromLog(logLine);
+    assertTrue("Didn't create HTTPLog", maybeHTTPLog.isPresent());
+    HTTPLog gotHTTPLog = maybeHTTPLog.get();
+    assertEquals(expected.getIp(), gotHTTPLog.getIp());
+    assertEquals(expected.getUser(), gotHTTPLog.getUser());
+    assertEquals(expected.getMethod(), gotHTTPLog.getMethod());
+    assertEquals(expected.getTimestamp(), gotHTTPLog.getTimestamp());
+    assertEquals(expected.getResource(), gotHTTPLog.getResource());
+    assertEquals(expected.getReferrer(), gotHTTPLog.getReferrer());
+    assertEquals(expected.getProtocol(), gotHTTPLog.getProtocol());
+    assertEquals(expected.getStatus(), gotHTTPLog.getStatus());
+    assertEquals(expected.getContentLength(), gotHTTPLog.getContentLength());
+    assertEquals(expected.getUserAgent(), gotHTTPLog.getUserAgent());
+  }
+
+  @Test
+  public void handleIPV6ParseHttpLog() {
+    String logLine =
+        "2405:204:a313:675::17a:b0b1 - - [19/Jan/2019:18:15:02 +0000] \"GET /plugins/codemirror-editor/static/codemirror_editor.js HTTP/1.1\" 200 1498 \"https://gerrithub.io/plugins/codemirror-editor/static/codemirror_editor.html\" \"Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko; googleweblight) Chrome/38.0.1025.166 Mobile Safari/535.19\"";
+    Optional<HTTPLog> maybeHTTPLog = HTTPLog.createFromLog(logLine);
+    assertTrue("Didn't create HTTPLog", maybeHTTPLog.isPresent());
+    assertEquals("2405:204:a313:675::17a:b0b1", maybeHTTPLog.get().getIp());
+  }
+
+  @Test(expected = Test.None.class /* no exception expected */)
+  public void handleNonNumericalContentLengthParseHttpLog() {
+    String logLine =
+        "171.13.14.52 - - [19/Jan/2019:00:00:46 +0000] \"HEAD /Documentation/index.html HTTP/1.1\" 200 - - \"Mozilla/5.0 (Windows NT 10.0 WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36\"";
+    HTTPLog.createFromLog(logLine);
+  }
+
+  @Test
+  public void invalidHttpLog() {
+    assertEquals(Optional.empty(), HTTPLog.createFromLog("invalid http log"));
+  }
+
+  @Test(expected = Test.None.class /* no exception expected */)
+  public void correctlyTransform_HttpAuditEvent() throws Exception {
+    HTTPLog auditHTTPLog =
+        new HTTPLog(
+            "10.10.100.108",
+            "anyUser",
+            "19/Jan/2019:00:00:00 +0000",
+            "amnyResource",
+            "anyProtocol",
+            "-",
+            200,
+            "-",
+            "-",
+            "\"git/1.8.3.1\"");
+
+    Optional<String> maybeAuditLog = auditHTTPLog.toAuditLog(loggerAudit);
+    assertTrue("Didn't Audit Log", maybeAuditLog.isPresent());
+
+    String auditLog = maybeAuditLog.get();
+    assertTrue(auditLog.contains("\"type\":\"HttpAuditEvent\""));
+    assertTrue(auditLog.contains("\"access_path\":\"GIT\""));
+  }
+
+  @Test(expected = Test.None.class /* no exception expected */)
+  public void correctlyTransform_ExtendedHttpAuditEvent() throws Exception {
+    HTTPLog auditHTTPLog =
+        new HTTPLog(
+            "10.10.100.108",
+            "anyUser",
+            "19/Jan/2019:00:00:00 +0000",
+            "amnyResource",
+            "anyProtocol",
+            "-",
+            200,
+            "-",
+            "-",
+            "\"anyUserAgent\"");
+
+    Optional<String> maybeAuditLog = auditHTTPLog.toAuditLog(loggerAudit);
+    assertTrue("Didn't Audit Log", maybeAuditLog.isPresent());
+
+    String auditLog = maybeAuditLog.get();
+    assertTrue(auditLog.contains("\"type\":\"ExtendedHttpAuditEvent\""));
+    assertTrue(auditLog.contains("\"access_path\":\"REST_API\""));
+  }
+}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/SSHLogTest.java b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/SSHLogTest.java
new file mode 100644
index 0000000..302f4b0
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/auditsl4j/logsource/SSHLogTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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 com.googlesource.gerrit.plugins.auditsl4j.logsource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.googlesource.gerrit.plugins.auditsl4j.AuditRendererToJson;
+import com.googlesource.gerrit.plugins.auditsl4j.AuditWriterToStringList;
+import com.googlesource.gerrit.plugins.auditsl4j.LoggerAudit;
+import java.util.Optional;
+import org.junit.Test;
+
+public class SSHLogTest {
+
+  LoggerAudit loggerAudit =
+      new LoggerAudit(new AuditWriterToStringList(), new AuditRendererToJson());
+
+  @Test
+  public void successfullyParseSshLog() {
+    String user = "myUser";
+    String timestamp = "2019-01-23 12:43:53,115 +0100";
+    String session = "b015fbe2";
+    String accountId = "a/1000000";
+    String command = "audit-sl4j.import.--from.rewew.--until.sdfds";
+    String waitTime = "2ms";
+    String execTime = "2ms";
+    String status = "0";
+
+    // [2019-01-23 12:44:04,723 +0100] b015fbe2 admin a/1000000
+    // audit-sl4j.import.--from.rewew.--until.sdfds 2ms 2ms 0
+    String logLine =
+        String.format(
+            "[%s] %s %s %s %s %s %s %s",
+            timestamp, session, user, accountId, command, waitTime, execTime, status);
+
+    SSHLog expected =
+        new SSHLog(timestamp, session, user, accountId, command, waitTime, execTime, status);
+
+    Optional<SSHLog> maybeSshLog = SSHLog.createFromLog(logLine);
+    assertTrue("Didn't create SSHLog", maybeSshLog.isPresent());
+    SSHLog gotSSHLog = maybeSshLog.get();
+    assertEquals(expected.getTimestamp(), gotSSHLog.getTimestamp());
+    assertEquals(expected.getSession(), gotSSHLog.getSession());
+    assertEquals(expected.getUser(), gotSSHLog.getUser());
+    assertEquals(expected.getAccountId(), gotSSHLog.getAccountId());
+    assertEquals(expected.getCommand(), gotSSHLog.getCommand());
+    assertEquals(expected.getWaitTime(), gotSSHLog.getWaitTime());
+    assertEquals(expected.getExecTime(), gotSSHLog.getExecTime());
+    assertEquals(expected.getStatus(), gotSSHLog.getStatus());
+  }
+
+  @Test
+  public void handleLogout() {
+    String logLine = "[2019-01-23 12:44:26,665 +0100] 70e3031f admin a/1000000 LOGOUT";
+    Optional<SSHLog> maybeSshLog = SSHLog.createFromLog(logLine);
+    assertTrue("Didn't create SSHLog", maybeSshLog.isPresent());
+    assertEquals("LOGOUT", maybeSshLog.get().getCommand());
+  }
+
+  @Test(expected = Test.None.class /* no exception expected */)
+  public void correctlyTransform_SshAuditEvent() throws Exception {
+    SSHLog SSHLog =
+        new SSHLog(
+            "2019-01-23 12:44:26,665 +0100",
+            "70e3031f",
+            "admin",
+            "a/1000000",
+            "LOGOUT",
+            "2ms",
+            "2ms",
+            "0");
+
+    Optional<String> maybeAuditLog = SSHLog.toAuditLog(loggerAudit);
+    assertTrue("Didn't Audit Log", maybeAuditLog.isPresent());
+
+    String auditLog = maybeAuditLog.get();
+    assertTrue("'type' not matched: " + auditLog, auditLog.contains("\"type\":\"SshAuditEvent\""));
+    assertTrue(
+        "'access_path' not matched: " + auditLog,
+        auditLog.contains("\"access_path\":\"SSH_COMMAND\""));
+    assertTrue("'when' not matched: " + auditLog, auditLog.contains("\"when\":1548243866665"));
+  }
+}