mirror of
https://github.com/8damon/Blackbird
synced 2026-08-09 11:56:04 +00:00
ui/session: update session storage and export flow
This commit is contained in:
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BlackbirdInterface
|
||||
{
|
||||
@@ -13,17 +14,19 @@ namespace BlackbirdInterface
|
||||
JsonLines,
|
||||
Csv,
|
||||
Cef,
|
||||
AttackCsv
|
||||
AttackCsv,
|
||||
DetectionJsonLines,
|
||||
DetectionCsv,
|
||||
DetectionCef,
|
||||
SplunkHecJson,
|
||||
ElasticEcsJsonLines
|
||||
}
|
||||
|
||||
internal static class SessionExportService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
WriteIndented = false
|
||||
};
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = false };
|
||||
|
||||
internal static void Export(string path, SessionFileArchive archive, SessionExportFormat format)
|
||||
internal static int Export(string path, SessionFileArchive archive, SessionExportFormat format)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
@@ -41,11 +44,15 @@ namespace BlackbirdInterface
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
List<SessionExportRecord> records = FlattenArchive(archive).ToList();
|
||||
List<SessionExportRecord> records =
|
||||
FlattenArchive(archive)
|
||||
.Where(record => !IsDetectionOnlyFormat(format) || IsDetectionRecord(record))
|
||||
.ToList();
|
||||
string tempPath = path + ".tmp";
|
||||
try
|
||||
{
|
||||
using var writer = new StreamWriter(tempPath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
using var writer =
|
||||
new StreamWriter(tempPath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
switch (format)
|
||||
{
|
||||
case SessionExportFormat.JsonLines:
|
||||
@@ -60,6 +67,21 @@ namespace BlackbirdInterface
|
||||
case SessionExportFormat.AttackCsv:
|
||||
WriteCsv(writer, records, includeAttackColumns: true);
|
||||
break;
|
||||
case SessionExportFormat.DetectionJsonLines:
|
||||
WriteSiemJsonLines(writer, records);
|
||||
break;
|
||||
case SessionExportFormat.DetectionCsv:
|
||||
WriteCsv(writer, records, includeAttackColumns: true);
|
||||
break;
|
||||
case SessionExportFormat.DetectionCef:
|
||||
WriteCef(writer, records);
|
||||
break;
|
||||
case SessionExportFormat.SplunkHecJson:
|
||||
WriteSplunkHecJson(writer, records);
|
||||
break;
|
||||
case SessionExportFormat.ElasticEcsJsonLines:
|
||||
WriteElasticEcsJsonLines(writer, records);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unsupported export format: {format}");
|
||||
}
|
||||
@@ -74,6 +96,8 @@ namespace BlackbirdInterface
|
||||
{
|
||||
File.Move(tempPath, path);
|
||||
}
|
||||
|
||||
return records.Count;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -92,18 +116,17 @@ namespace BlackbirdInterface
|
||||
|
||||
foreach (TelemetryEvent ev in tab.Events.OrderBy(x => x.TimestampUtc))
|
||||
{
|
||||
yield return new SessionExportRecord
|
||||
{
|
||||
TimestampUtc = ev.TimestampUtc,
|
||||
Tab = tabTitle,
|
||||
Pid = ev.PID != 0 ? ev.PID : tab.Pid,
|
||||
Tid = ev.TID,
|
||||
Stream = "timeline",
|
||||
EventName = string.IsNullOrWhiteSpace(ev.SubType) ? ev.Group : $"{ev.Group}/{ev.SubType}",
|
||||
Source = ev.ProcessName,
|
||||
Summary = ev.Summary,
|
||||
Details = ev.Details
|
||||
};
|
||||
yield return new SessionExportRecord { TimestampUtc = ev.TimestampUtc,
|
||||
Tab = tabTitle,
|
||||
Pid = ev.PID != 0 ? ev.PID : tab.Pid,
|
||||
Tid = ev.TID,
|
||||
Stream = "timeline",
|
||||
EventName = string.IsNullOrWhiteSpace(ev.SubType)
|
||||
? ev.Group
|
||||
: $"{ev.Group}/{ev.SubType}",
|
||||
Source = ev.ProcessName,
|
||||
Summary = ev.Summary,
|
||||
Details = ev.Details };
|
||||
}
|
||||
|
||||
foreach (SessionExportRecord record in FlattenGroupedRows(tabTitle, tab.Pid, "etw", tab.EtwGroups))
|
||||
@@ -111,51 +134,71 @@ namespace BlackbirdInterface
|
||||
yield return record;
|
||||
}
|
||||
|
||||
foreach (SessionExportRecord record in FlattenGroupedRows(tabTitle, tab.Pid, "heuristics", tab.HeuristicsGroups))
|
||||
foreach (SessionExportRecord record in FlattenGroupedRows(tabTitle, tab.Pid, "heuristics",
|
||||
tab.HeuristicsGroups))
|
||||
{
|
||||
yield return record;
|
||||
}
|
||||
|
||||
foreach (SessionExportRecord record in FlattenGroupedRows(tabTitle, tab.Pid, "filesystem", tab.FilesystemGroups))
|
||||
foreach (SessionExportRecord record in FlattenGroupedRows(tabTitle, tab.Pid, "filesystem",
|
||||
tab.FilesystemGroups))
|
||||
{
|
||||
yield return record;
|
||||
}
|
||||
|
||||
foreach (SessionExportRecord record in FlattenGroupedRows(tabTitle, tab.Pid, "relations", tab.ProcessRelationsGroups))
|
||||
foreach (SessionExportRecord record in FlattenGroupedRows(tabTitle, tab.Pid, "relations",
|
||||
tab.ProcessRelationsGroups))
|
||||
{
|
||||
yield return record;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<SessionExportRecord> FlattenGroupedRows(
|
||||
string tabTitle,
|
||||
int defaultPid,
|
||||
string stream,
|
||||
IEnumerable<GroupedEventRow> rows)
|
||||
private static bool IsDetectionOnlyFormat(SessionExportFormat format) =>
|
||||
format is SessionExportFormat.DetectionJsonLines or SessionExportFormat.DetectionCsv or SessionExportFormat
|
||||
.DetectionCef or SessionExportFormat.SplunkHecJson or SessionExportFormat.ElasticEcsJsonLines;
|
||||
|
||||
private static bool IsDetectionRecord(SessionExportRecord record)
|
||||
{
|
||||
if (record.Stream.Equals("heuristics", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!record.Stream.Equals("etw", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string combined = $"{record.EventName} {record.Detection} {record.Summary} {record.Details}";
|
||||
return combined.Contains("Detection", StringComparison.OrdinalIgnoreCase) ||
|
||||
combined.Contains("detection=", StringComparison.OrdinalIgnoreCase) ||
|
||||
combined.Contains("engine=sigma", StringComparison.OrdinalIgnoreCase) ||
|
||||
combined.Contains("engine=yara", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static IEnumerable<SessionExportRecord>
|
||||
FlattenGroupedRows(string tabTitle, int defaultPid, string stream, IEnumerable<GroupedEventRow> rows)
|
||||
{
|
||||
foreach (GroupedEventRow row in rows.OrderBy(x => x.LastSeenUtc))
|
||||
{
|
||||
if (row.Details.Count == 0)
|
||||
{
|
||||
yield return new SessionExportRecord
|
||||
{
|
||||
TimestampUtc = row.LastSeenUtc,
|
||||
Tab = tabTitle,
|
||||
Pid = defaultPid,
|
||||
Stream = stream,
|
||||
EventName = row.Event,
|
||||
Severity = row.Severity,
|
||||
Detection = row.Detection,
|
||||
Summary = row.GroupKey
|
||||
};
|
||||
yield return new SessionExportRecord { TimestampUtc = row.LastSeenUtc,
|
||||
Tab = tabTitle,
|
||||
Pid = defaultPid,
|
||||
Stream = stream,
|
||||
EventName = row.Event,
|
||||
Severity = row.Severity,
|
||||
Detection = row.Detection,
|
||||
Summary = row.GroupKey,
|
||||
Hits = Math.Max(1, row.Hits) };
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (GroupedEventDetailRow detail in row.Details.OrderBy(x => x.TimestampUtc))
|
||||
{
|
||||
yield return new SessionExportRecord
|
||||
{
|
||||
yield return new SessionExportRecord {
|
||||
TimestampUtc = detail.TimestampUtc,
|
||||
Tab = tabTitle,
|
||||
Pid = detail.ActorPid != 0 ? unchecked((int)detail.ActorPid) : defaultPid,
|
||||
@@ -170,7 +213,8 @@ namespace BlackbirdInterface
|
||||
ActorPid = detail.ActorPid,
|
||||
TargetPid = detail.TargetPid,
|
||||
Summary = $"{detail.Event} {detail.Detection}".Trim(),
|
||||
Details = detail.Details
|
||||
Details = detail.Details,
|
||||
Hits = Math.Max(1, detail.HitCount)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -184,27 +228,50 @@ namespace BlackbirdInterface
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteCsv(TextWriter writer, IEnumerable<SessionExportRecord> records, bool includeAttackColumns)
|
||||
private static void WriteSiemJsonLines(TextWriter writer, IEnumerable<SessionExportRecord> records)
|
||||
{
|
||||
var headers = new List<string>
|
||||
foreach (SessionExportRecord record in records)
|
||||
{
|
||||
"timestamp_utc", "tab", "pid", "tid", "stream", "event", "severity", "detection",
|
||||
"source", "actor", "actor_pid", "target", "target_pid", "summary", "details"
|
||||
};
|
||||
writer.WriteLine(JsonSerializer.Serialize(BuildSiemEvent(record), JsonOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteSplunkHecJson(TextWriter writer, IEnumerable<SessionExportRecord> records)
|
||||
{
|
||||
foreach (SessionExportRecord record in records)
|
||||
{
|
||||
var hec = new Dictionary < string, object
|
||||
?> { ["time"] = ToUnixTimeSeconds(record.TimestampUtc), ["host"] = Environment.MachineName,
|
||||
["source"] = "BK", ["sourcetype"] = "BK:detection", ["event"] = BuildSiemEvent(record) };
|
||||
writer.WriteLine(JsonSerializer.Serialize(hec, JsonOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteElasticEcsJsonLines(TextWriter writer, IEnumerable<SessionExportRecord> records)
|
||||
{
|
||||
foreach (SessionExportRecord record in records)
|
||||
{
|
||||
writer.WriteLine(JsonSerializer.Serialize(BuildElasticEcsEvent(record), JsonOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteCsv(TextWriter writer, IEnumerable<SessionExportRecord> records,
|
||||
bool includeAttackColumns)
|
||||
{
|
||||
var headers = new List<string> { "timestamp_utc", "tab", "pid", "tid", "stream",
|
||||
"event", "severity", "detection", "source", "actor",
|
||||
"actor_pid", "target", "target_pid", "summary", "details" };
|
||||
|
||||
if (includeAttackColumns)
|
||||
{
|
||||
headers.AddRange(new[]
|
||||
{
|
||||
"mitre_tactic", "mitre_technique_id", "mitre_technique", "mitre_subtechnique_id", "mitre_subtechnique"
|
||||
});
|
||||
headers.AddRange(new[] { "mitre_tactic", "mitre_technique_id", "mitre_technique",
|
||||
"mitre_subtechnique_id", "mitre_subtechnique" });
|
||||
}
|
||||
|
||||
writer.WriteLine(string.Join(",", headers.Select(EscapeCsv)));
|
||||
foreach (SessionExportRecord record in records)
|
||||
{
|
||||
var values = new List<string>
|
||||
{
|
||||
var values = new List<string> {
|
||||
record.TimestampUtc.ToString("O", CultureInfo.InvariantCulture),
|
||||
record.Tab,
|
||||
record.Pid.ToString(CultureInfo.InvariantCulture),
|
||||
@@ -224,7 +291,9 @@ namespace BlackbirdInterface
|
||||
|
||||
if (includeAttackColumns)
|
||||
{
|
||||
values.AddRange(new[] { string.Empty, string.Empty, string.Empty, string.Empty, string.Empty });
|
||||
DetectionMetadata metadata = ExtractDetectionMetadata(record);
|
||||
values.AddRange(
|
||||
new[] { string.Empty, metadata.MitreTechniqueId, string.Empty, string.Empty, string.Empty });
|
||||
}
|
||||
|
||||
writer.WriteLine(string.Join(",", values.Select(EscapeCsv)));
|
||||
@@ -235,39 +304,162 @@ namespace BlackbirdInterface
|
||||
{
|
||||
foreach (SessionExportRecord record in records)
|
||||
{
|
||||
DetectionMetadata metadata = ExtractDetectionMetadata(record);
|
||||
string signature = string.IsNullOrWhiteSpace(record.Detection) ? record.EventName : record.Detection;
|
||||
string name = string.IsNullOrWhiteSpace(record.Summary) ? signature : record.Summary;
|
||||
int severity = MapSeverity(record.Severity);
|
||||
string extension =
|
||||
$"rt={EscapeCefExtension(record.TimestampUtc.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture))} " +
|
||||
$"cs1Label=stream cs1={EscapeCefExtension(record.Stream)} " +
|
||||
$"sproc={EscapeCefExtension(record.Source)} " +
|
||||
$"dproc={EscapeCefExtension(record.Target)} " +
|
||||
$"src={EscapeCefExtension(record.Actor)} " +
|
||||
$"dst={EscapeCefExtension(record.Target)} " +
|
||||
$"cs2Label=rule_engine cs2={EscapeCefExtension(metadata.Engine)} " +
|
||||
$"cs3Label=mitre_technique_id cs3={EscapeCefExtension(metadata.MitreTechniqueId)} " +
|
||||
$"cs4Label=rule_id cs4={EscapeCefExtension(metadata.RuleId)} " +
|
||||
$"cn1Label=actor_pid cn1={record.ActorPid} " + $"cn2Label=target_pid cn2={record.TargetPid} " +
|
||||
$"sproc={EscapeCefExtension(record.Source)} " + $"dproc={EscapeCefExtension(record.Target)} " +
|
||||
$"src={EscapeCefExtension(record.Actor)} " + $"dst={EscapeCefExtension(record.Target)} " +
|
||||
$"msg={EscapeCefExtension(record.Details)} " +
|
||||
$"deviceProcessName={EscapeCefExtension(record.Tab)} " +
|
||||
$"externalId={EscapeCefExtension(record.Pid.ToString(CultureInfo.InvariantCulture))}";
|
||||
|
||||
writer.WriteLine(
|
||||
$"CEF:0|Blackbird|Platform|1.0|{EscapeCefHeader(signature)}|{EscapeCefHeader(name)}|{severity}|{extension}");
|
||||
$"CEF:0|BK|Platform|1.0|{EscapeCefHeader(signature)}|{EscapeCefHeader(name)}|{severity}|{extension}");
|
||||
}
|
||||
}
|
||||
|
||||
private static int TryParseInt(string value)
|
||||
=> int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) ? parsed : 0;
|
||||
private static Dictionary<string, object?> BuildSiemEvent(SessionExportRecord record)
|
||||
{
|
||||
DetectionMetadata metadata = ExtractDetectionMetadata(record);
|
||||
int severity = MapSeverity(record.Severity);
|
||||
return new Dictionary < string,
|
||||
object ?> { ["@timestamp"] = record.TimestampUtc.ToString("O", CultureInfo.InvariantCulture),
|
||||
["host.name"] = Environment.MachineName,
|
||||
["event.kind"] = "alert",
|
||||
["event.module"] = "BK",
|
||||
["event.dataset"] = "BK.detections",
|
||||
["event.action"] = record.EventName,
|
||||
["event.severity"] = severity,
|
||||
["event.count"] = Math.Max(1, record.Hits),
|
||||
["rule.id"] = metadata.RuleId,
|
||||
["rule.name"] = metadata.RuleName,
|
||||
["rule.ruleset"] = metadata.Engine,
|
||||
["rule.category"] = metadata.Category,
|
||||
["threat.technique.id"] = metadata.MitreTechniqueId,
|
||||
["observer.vendor"] = "BK",
|
||||
["observer.product"] = "BK Analysis Interface",
|
||||
["BK.tab"] = record.Tab,
|
||||
["BK.stream"] = record.Stream,
|
||||
["BK.summary"] = record.Summary,
|
||||
["BK.details"] = record.Details,
|
||||
["detection.name"] = record.Detection,
|
||||
["severity.label"] = record.Severity,
|
||||
["source.process.name"] = record.Actor,
|
||||
["source.process.pid"] = OptionalNumber(record.ActorPid),
|
||||
["target.process.name"] = record.Target,
|
||||
["target.process.pid"] = OptionalNumber(record.TargetPid),
|
||||
["process.name"] = record.Source,
|
||||
["process.pid"] = OptionalNumber(record.Pid),
|
||||
["process.thread.id"] = OptionalNumber(record.Tid) };
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> BuildElasticEcsEvent(SessionExportRecord record)
|
||||
{
|
||||
DetectionMetadata metadata = ExtractDetectionMetadata(record);
|
||||
int severity = MapSeverity(record.Severity);
|
||||
var threat = string.IsNullOrWhiteSpace(metadata.MitreTechniqueId) ? null : new Dictionary < string,
|
||||
object ?> { ["technique"] =
|
||||
new[] { new Dictionary < string, object ?> { ["id"] = metadata.MitreTechniqueId } } };
|
||||
|
||||
return new Dictionary < string,
|
||||
object ?> { ["@timestamp"] = record.TimestampUtc.ToString("O", CultureInfo.InvariantCulture),
|
||||
["ecs"] = new Dictionary < string, object ?> { ["version"] = "8.11.0" },
|
||||
["host"] = new Dictionary < string, object ?> { ["name"] = Environment.MachineName },
|
||||
["event"] = new Dictionary < string,
|
||||
object ?> { ["kind"] = "alert", ["category"] = new[] { "malware" }, ["type"] = new[] { "indicator" },
|
||||
["module"] = "BK", ["dataset"] = "BK.detections", ["action"] = record.EventName,
|
||||
["severity"] = severity, ["risk_score"] = severity, ["reason"] = record.Summary,
|
||||
["count"] = Math.Max(1, record.Hits) },
|
||||
["rule"] = new Dictionary < string,
|
||||
object ?> { ["id"] = metadata.RuleId, ["name"] = metadata.RuleName, ["ruleset"] = metadata.Engine,
|
||||
["category"] = metadata.Category },
|
||||
["observer"] = new Dictionary < string,
|
||||
object ?> { ["vendor"] = "BK", ["product"] = "BK Analysis Interface", ["type"] = "sensor" },
|
||||
["process"] = new Dictionary < string,
|
||||
object ?> { ["name"] = record.Source, ["pid"] = OptionalNumber(record.Pid),
|
||||
["thread"] = new Dictionary < string, object ?> { ["id"] = OptionalNumber(record.Tid) } },
|
||||
["source"] = new Dictionary < string,
|
||||
object ?> { ["process"] = new Dictionary < string,
|
||||
object ?> { ["name"] = record.Actor, ["pid"] = OptionalNumber(record.ActorPid) } },
|
||||
["target"] = new Dictionary < string,
|
||||
object ?> { ["process"] = new Dictionary < string,
|
||||
object ?> { ["name"] = record.Target, ["pid"] = OptionalNumber(record.TargetPid) } },
|
||||
["threat"] = threat, ["BK"] = new Dictionary < string,
|
||||
object ?> { ["tab"] = record.Tab, ["stream"] = record.Stream, ["detection"] = record.Detection,
|
||||
["severity_label"] = record.Severity, ["details"] = record.Details } };
|
||||
}
|
||||
|
||||
private static DetectionMetadata ExtractDetectionMetadata(SessionExportRecord record)
|
||||
{
|
||||
string combined = $"{record.Details} {record.Summary} {record.Detection}";
|
||||
string engine =
|
||||
FirstNonEmpty(ExtractKeyValue(combined, "engine"),
|
||||
record.Detection.StartsWith("SIGMA_", StringComparison.OrdinalIgnoreCase) ? "sigma"
|
||||
: record.Detection.StartsWith("YARA_", StringComparison.OrdinalIgnoreCase) ? "yara"
|
||||
: "BK");
|
||||
string ruleId = FirstNonEmpty(ExtractKeyValue(combined, "sigma_id"), ExtractKeyValue(combined, "rule"),
|
||||
record.Detection);
|
||||
string ruleName = FirstNonEmpty(DecodeEvidenceToken(ExtractKeyValue(combined, "rule")), record.Detection,
|
||||
record.EventName);
|
||||
string category = FirstNonEmpty(ExtractKeyValue(combined, "category"), record.Stream);
|
||||
string mitreTechniqueId = ExtractMitreTechniqueId(combined);
|
||||
return new DetectionMetadata(engine, ruleId, ruleName, category, mitreTechniqueId);
|
||||
}
|
||||
|
||||
private static string ExtractKeyValue(string text, string key)
|
||||
{
|
||||
Match match = Regex.Match(text ?? string.Empty, $@"(?:^|\s){Regex.Escape(key)}=(?<value>[^\s]+)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
return match.Success ? match.Groups["value"].Value.Trim() : string.Empty;
|
||||
}
|
||||
|
||||
private static string DecodeEvidenceToken(string value) => (value ?? string.Empty).Trim().Replace('_', ' ');
|
||||
|
||||
private static string ExtractMitreTechniqueId(string text)
|
||||
{
|
||||
Match match = Regex.Match(text ?? string.Empty, @"\bT\d{4}(?:\.\d{3})?\b",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
return match.Success ? match.Value.ToUpperInvariant() : string.Empty;
|
||||
}
|
||||
|
||||
private static string FirstNonEmpty(params string[] values) =>
|
||||
values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x))?.Trim() ?? string.Empty;
|
||||
|
||||
private static double ToUnixTimeSeconds(DateTime timestampUtc)
|
||||
{
|
||||
DateTime utc = timestampUtc.Kind == DateTimeKind.Utc ? timestampUtc
|
||||
: DateTime.SpecifyKind(timestampUtc, DateTimeKind.Utc);
|
||||
return new DateTimeOffset(utc).ToUnixTimeMilliseconds() / 1000.0;
|
||||
}
|
||||
|
||||
private static object? OptionalNumber(int value) => value == 0 ? null : value;
|
||||
|
||||
private static object? OptionalNumber(uint value) => value == 0 ? null : value;
|
||||
|
||||
private static int TryParseInt(string value) => int.TryParse(value, NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture, out int parsed)
|
||||
? parsed
|
||||
: 0;
|
||||
|
||||
private static int MapSeverity(string severity)
|
||||
{
|
||||
return (severity ?? string.Empty).Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"critical" => 10,
|
||||
"high" => 8,
|
||||
"medium" => 5,
|
||||
"low" => 3,
|
||||
"info" => 1,
|
||||
_ => 4
|
||||
};
|
||||
return (severity ?? string.Empty)
|
||||
.Trim()
|
||||
.ToLowerInvariant() switch { "critical" => 10,
|
||||
"high" => 8,
|
||||
"medium" => 5,
|
||||
"low" => 3,
|
||||
"informational" => 1,
|
||||
"info" => 1,
|
||||
_ => 4 };
|
||||
}
|
||||
|
||||
private static string EscapeCsv(string value)
|
||||
@@ -281,15 +473,11 @@ namespace BlackbirdInterface
|
||||
return "\"" + text.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private static string EscapeCefHeader(string value)
|
||||
=> (value ?? string.Empty).Replace("\\", "\\\\").Replace("|", "\\|");
|
||||
private static string
|
||||
EscapeCefHeader(string value) => (value ?? string.Empty).Replace("\\", "\\\\").Replace("|", "\\|");
|
||||
|
||||
private static string EscapeCefExtension(string value)
|
||||
=> (value ?? string.Empty)
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("=", "\\=")
|
||||
.Replace("\r", " ")
|
||||
.Replace("\n", " ");
|
||||
private static string EscapeCefExtension(string value) =>
|
||||
(value ?? string.Empty).Replace("\\", "\\\\").Replace("=", "\\=").Replace("\r", " ").Replace("\n", " ");
|
||||
|
||||
private sealed class SessionExportRecord
|
||||
{
|
||||
@@ -308,7 +496,10 @@ namespace BlackbirdInterface
|
||||
public uint TargetPid { get; init; }
|
||||
public string Summary { get; init; } = string.Empty;
|
||||
public string Details { get; init; } = string.Empty;
|
||||
public int Hits { get; init; } = 1;
|
||||
}
|
||||
|
||||
private sealed record DetectionMetadata(string Engine, string RuleId, string RuleName, string Category,
|
||||
string MitreTechniqueId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,16 @@ namespace BlackbirdInterface
|
||||
public double ViewStartSeconds { get; set; }
|
||||
public string? LaneFocusKey { get; set; }
|
||||
public bool UseUsermodeHooks { get; set; }
|
||||
public bool KernelHooksEnabled { get; set; } = true;
|
||||
public bool SignatureIntelEnabled { get; set; } = true;
|
||||
public bool SignatureIntelMemoryScanEnabled { get; set; }
|
||||
public bool SignatureIntelPageScanEnabled { get; set; }
|
||||
public bool TargetExited { get; set; }
|
||||
public string TargetExitReason { get; set; } = "";
|
||||
public bool OfflineSnapshot { get; set; } = true;
|
||||
public LaunchTargetKind AnalysisSubjectKind { get; set; } = LaunchTargetKind.Executable;
|
||||
public string AnalysisSubjectPath { get; set; } = "";
|
||||
public string AnalysisHostPath { get; set; } = "";
|
||||
|
||||
[JsonIgnore]
|
||||
public string? CaptureStorePath { get; set; }
|
||||
@@ -39,8 +47,10 @@ namespace BlackbirdInterface
|
||||
public List<GroupedEventRow> EtwGroups { get; set; } = new();
|
||||
public List<GroupedEventRow> HeuristicsGroups { get; set; } = new();
|
||||
public List<GroupedEventRow> FilesystemGroups { get; set; } = new();
|
||||
public List<GroupedEventRow> RegistryGroups { get; set; } = new();
|
||||
public List<GroupedEventRow> ProcessRelationsGroups { get; set; } = new();
|
||||
public List<ApiCallGraphRowSnapshot> ApiGraphRows { get; set; } = new();
|
||||
public List<ExtendedActivityRowSnapshot> ExtendedActivityRows { get; set; } = new();
|
||||
public List<ThreadStackHistoryArchiveEntry> ThreadStackHistories { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -57,14 +67,11 @@ namespace BlackbirdInterface
|
||||
private const int MaxThreadStackHistoriesPerTab = 8_192;
|
||||
private const int MaxThreadStackSnapshotsPerHistory = 2_048;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = false
|
||||
};
|
||||
private static readonly JsonSerializerOptions JsonOptions =
|
||||
new() { PropertyNameCaseInsensitive = true, WriteIndented = false };
|
||||
|
||||
internal static bool Exists(string? path)
|
||||
=> !string.IsNullOrWhiteSpace(path) && CaptureArchiveStorage.Exists(path);
|
||||
internal static bool
|
||||
Exists(string? path) => !string.IsNullOrWhiteSpace(path) && CaptureArchiveStorage.Exists(path);
|
||||
|
||||
internal static void DeletePath(string? path)
|
||||
{
|
||||
@@ -121,8 +128,7 @@ namespace BlackbirdInterface
|
||||
SessionFileArchive archive = workspace.Archive;
|
||||
if (archive.Version <= 0 || archive.Version > CurrentVersion)
|
||||
{
|
||||
throw new InvalidDataException(
|
||||
$"Unsupported session archive version ({archive.Version}).");
|
||||
throw new InvalidDataException($"Unsupported session archive version ({archive.Version}).");
|
||||
}
|
||||
|
||||
NormalizeArchive(archive);
|
||||
@@ -162,7 +168,8 @@ namespace BlackbirdInterface
|
||||
$"Session archive exceeds compressed size limit ({MaxCompressedArchiveBytes / (1024 * 1024)} MB).");
|
||||
}
|
||||
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024, FileOptions.SequentialScan);
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 64 * 1024,
|
||||
FileOptions.SequentialScan);
|
||||
using var gzip = new GZipStream(stream, CompressionMode.Decompress);
|
||||
using var bounded = new BoundedReadStream(gzip, MaxUncompressedArchiveBytes);
|
||||
|
||||
@@ -183,6 +190,9 @@ namespace BlackbirdInterface
|
||||
{
|
||||
tab.Title ??= string.Empty;
|
||||
tab.LaneFocusKey ??= null;
|
||||
tab.TargetExitReason ??= string.Empty;
|
||||
tab.AnalysisSubjectPath ??= string.Empty;
|
||||
tab.AnalysisHostPath ??= string.Empty;
|
||||
tab.Events ??= new List<TelemetryEvent>();
|
||||
tab.PerformanceHistory ??= new List<PerformanceSample>();
|
||||
tab.MemoryRegionAttributionHistory ??= new List<MemoryRegionAttributionSample>();
|
||||
@@ -190,8 +200,10 @@ namespace BlackbirdInterface
|
||||
tab.EtwGroups ??= new List<GroupedEventRow>();
|
||||
tab.HeuristicsGroups ??= new List<GroupedEventRow>();
|
||||
tab.FilesystemGroups ??= new List<GroupedEventRow>();
|
||||
tab.RegistryGroups ??= new List<GroupedEventRow>();
|
||||
tab.ProcessRelationsGroups ??= new List<GroupedEventRow>();
|
||||
tab.ApiGraphRows ??= new List<ApiCallGraphRowSnapshot>();
|
||||
tab.ExtendedActivityRows ??= new List<ExtendedActivityRowSnapshot>();
|
||||
tab.ThreadStackHistories ??= new List<ThreadStackHistoryArchiveEntry>();
|
||||
tab.CaptureStorePath ??= null;
|
||||
}
|
||||
@@ -223,8 +235,10 @@ namespace BlackbirdInterface
|
||||
if (tab.EtwGroups.Count > MaxGroupedRowsPerCategory ||
|
||||
tab.HeuristicsGroups.Count > MaxGroupedRowsPerCategory ||
|
||||
tab.FilesystemGroups.Count > MaxGroupedRowsPerCategory ||
|
||||
tab.RegistryGroups.Count > MaxGroupedRowsPerCategory ||
|
||||
tab.ProcessRelationsGroups.Count > MaxGroupedRowsPerCategory ||
|
||||
tab.ApiGraphRows.Count > MaxGroupedRowsPerCategory)
|
||||
tab.ApiGraphRows.Count > MaxGroupedRowsPerCategory ||
|
||||
tab.ExtendedActivityRows.Count > MaxGroupedRowsPerCategory)
|
||||
{
|
||||
throw new InvalidDataException($"PID {tab.Pid} has too many grouped intel rows.");
|
||||
}
|
||||
@@ -237,7 +251,8 @@ namespace BlackbirdInterface
|
||||
{
|
||||
if (history.Snapshots.Count > MaxThreadStackSnapshotsPerHistory)
|
||||
{
|
||||
throw new InvalidDataException($"PID {tab.Pid} TID {history.Tid} has too many thread stack snapshots.");
|
||||
throw new InvalidDataException(
|
||||
$"PID {tab.Pid} TID {history.Tid} has too many thread stack snapshots.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,4 +346,3 @@ namespace BlackbirdInterface
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,8 @@ namespace BlackbirdInterface
|
||||
string rootPath;
|
||||
if (_currentSession != null && _currentSession.Pid == pid)
|
||||
{
|
||||
rootPath = IsSessionCachePath(_currentSession.BackingStorePath)
|
||||
? _currentSession.BackingStorePath!
|
||||
: AllocateSessionCachePath(pid);
|
||||
rootPath = IsSessionCachePath(_currentSession.BackingStorePath) ? _currentSession.BackingStorePath!
|
||||
: AllocateSessionCachePath(pid);
|
||||
_currentSession.BackingStorePath = rootPath;
|
||||
}
|
||||
else
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user