@@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
1515import kotlinx.coroutines.async
1616import kotlinx.coroutines.coroutineScope
1717import kotlinx.coroutines.withContext
18+ import kotlinx.serialization.json.Json
1819import kotlinx.serialization.json.JsonArray
1920import kotlinx.serialization.json.JsonElement
2021import kotlinx.serialization.json.JsonNull
@@ -34,7 +35,7 @@ data class MaltrailCountPoint(
3435) {
3536 val apiDate: String
3637 get() = Instant .ofEpochSecond(timestamp)
37- .atZone(ZoneOffset . UTC )
38+ .atZone(ZoneId .systemDefault() )
3839 .toLocalDate()
3940 .format(DateTimeFormatter .ISO_LOCAL_DATE )
4041
@@ -149,7 +150,7 @@ class MaltrailRepository @Inject constructor(
149150 }
150151
151152 suspend fun getEvents (instanceId : String , date : String ): List <MaltrailEvent > {
152- return parseEvents(api.getEvents(instanceId = instanceId, date = date))
153+ return parseEvents(api.getEvents(instanceId = instanceId, date = date).string() )
153154 }
154155
155156 suspend fun getSummary (instanceId : String ): MaltrailSummary {
@@ -236,6 +237,11 @@ class MaltrailRepository @Inject constructor(
236237 }.sortedByDescending { it.timestamp }
237238 }
238239
240+ private fun parseEvents (raw : String ): List <MaltrailEvent > {
241+ val jsonEvents = runCatching { parseEvents(Json .parseToJsonElement(raw)) }.getOrDefault(emptyList())
242+ return jsonEvents.ifEmpty { parseTextEvents(raw) }
243+ }
244+
239245 private fun parseEvents (element : JsonElement ): List <MaltrailEvent > {
240246 return extractObjectArray(element, " events" , " data" , " items" , " results" ).mapIndexed { index, obj ->
241247 val raw = obj.toFlatStringMap()
@@ -263,6 +269,109 @@ class MaltrailRepository @Inject constructor(
263269 }
264270 }
265271
272+ private fun parseTextEvents (raw : String ): List <MaltrailEvent > {
273+ return raw.lineSequence()
274+ .map { it.trim() }
275+ .filter { it.isNotBlank() }
276+ .mapIndexedNotNull { index, line ->
277+ val parts = splitLogLine(line)
278+ if (parts.size < 10 ) {
279+ return @mapIndexedNotNull MaltrailEvent (
280+ id = " raw-$index -${line.hashCode()} " ,
281+ timestamp = null ,
282+ source = null ,
283+ destination = null ,
284+ protocolName = null ,
285+ trail = null ,
286+ severity = null ,
287+ sensor = null ,
288+ info = line,
289+ rawFields = mapOf (" raw" to line)
290+ )
291+ }
292+
293+ val hasSplitTimestamp = parts.getOrNull(0 )?.matches(Regex (" \\ d{4}-\\ d{2}-\\ d{2}" )) == true &&
294+ parts.getOrNull(1 )?.contains(" :" ) == true
295+ val offset = if (hasSplitTimestamp) 1 else 0
296+ val timestamp = if (hasSplitTimestamp) " ${parts[0 ]} ${parts[1 ]} " else parts[0 ]
297+ val sensor = parts.getOrNull(1 + offset)
298+ val source = parts.getOrNull(2 + offset)
299+ val sourcePort = parts.getOrNull(3 + offset)
300+ val destination = parts.getOrNull(4 + offset)
301+ val destinationPort = parts.getOrNull(5 + offset)
302+ val protocol = parts.getOrNull(6 + offset)
303+ val trailType = parts.getOrNull(7 + offset)
304+ val trail = parts.getOrNull(8 + offset)
305+ val info = parts.drop(9 + offset).joinToString(" " ).ifBlank { null }
306+ val rawFields = linkedMapOf(
307+ " timestamp" to timestamp,
308+ " sensor" to sensor,
309+ " src_ip" to source,
310+ " src_port" to sourcePort,
311+ " dst_ip" to destination,
312+ " dst_port" to destinationPort,
313+ " protocol" to protocol,
314+ " type" to trailType,
315+ " trail" to trail,
316+ " info" to info
317+ ).mapNotNull { (key, value) -> value?.takeIf { it.isNotBlank() }?.let { key to it } }.toMap()
318+
319+ MaltrailEvent (
320+ id = " $timestamp |$source |$destination |$trail |$index " ,
321+ timestamp = timestamp,
322+ source = source,
323+ destination = destination,
324+ protocolName = protocol,
325+ trail = trail,
326+ severity = inferSeverity(info, trailType),
327+ sensor = sensor,
328+ info = info,
329+ rawFields = rawFields
330+ )
331+ }
332+ .toList()
333+ }
334+
335+ private fun splitLogLine (line : String ): List <String > {
336+ val result = mutableListOf<String >()
337+ val current = StringBuilder ()
338+ var inQuotes = false
339+ var escaping = false
340+
341+ for (char in line) {
342+ when {
343+ escaping -> {
344+ current.append(char)
345+ escaping = false
346+ }
347+ char == ' \\ ' && inQuotes -> escaping = true
348+ char == ' "' -> inQuotes = ! inQuotes
349+ char.isWhitespace() && ! inQuotes -> {
350+ if (current.isNotEmpty()) {
351+ result + = current.toString()
352+ current.clear()
353+ }
354+ }
355+ else -> current.append(char)
356+ }
357+ }
358+
359+ if (current.isNotEmpty()) {
360+ result + = current.toString()
361+ }
362+ return result
363+ }
364+
365+ private fun inferSeverity (info : String? , trailType : String? ): String? {
366+ val value = " ${info.orEmpty()} ${trailType.orEmpty()} " .lowercase(Locale .ROOT )
367+ return when {
368+ " malware" in value || " ransom" in value || " trojan" in value -> " high"
369+ " malicious" in value || " attack" in value || " scanner" in value -> " medium"
370+ " suspicious" in value -> " low"
371+ else -> null
372+ }
373+ }
374+
266375 private fun extractObjectArray (element : JsonElement , vararg keys : String ): List <JsonObject > {
267376 when (element) {
268377 is JsonArray -> return element.mapNotNull { it as ? JsonObject }
0 commit comments