Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 125 additions & 7 deletions src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@

namespace Elastic.Markdown.Extensions.DetectionRules;

public record DetectionRuleThreat
{
public required string Framework { get; init; }
public required DetectionRuleTechnique[] Techniques { get; init; } = [];
public required DetectionRuleTactic Tactic { get; init; }
}

public record DetectionRuleTactic
{
public required string Id { get; init; }
public required string Name { get; init; }
public required string Reference { get; init; }
}

public record DetectionRuleSubTechnique
{
public required string Id { get; init; }
public required string Name { get; init; }
public required string Reference { get; init; }
}

public record DetectionRuleTechnique : DetectionRuleSubTechnique
{
public required DetectionRuleSubTechnique[] SubTechniques { get; init; } = [];
}

public record DetectionRule
{
public required string Name { get; init; }
Expand All @@ -19,6 +45,8 @@ public record DetectionRule

public required string? Query { get; init; }

public required string? Setup { get; init; }

public required string[]? Tags { get; init; }

public required string Severity { get; init; }
Expand All @@ -39,6 +67,8 @@ public record DetectionRule
public required string[]? References { get; init; }
public required string Version { get; init; }

public required DetectionRuleThreat[] Threats { get; init; } = [];

public static DetectionRule From(IFileInfo source)
{
TomlDocument model;
Expand All @@ -51,32 +81,124 @@ public static DetectionRule From(IFileInfo source)
{
throw new Exception($"Could not parse toml in: {source.FullName}", e);
}

if (!model.TryGetValue("metadata", out var node) || node is not TomlTable metadata)
throw new Exception($"Could not find metadata section in {source.FullName}");

if (!model.TryGetValue("rule", out node) || node is not TomlTable rule)
throw new Exception($"Could not find rule section in {source.FullName}");

var threats = GetThreats(rule);

return new DetectionRule
{
Authors = TryGetStringArray(rule, "author"),
Description = rule.GetString("description"),
Type = rule.GetString("type"),
Language = TryGetString(rule, "language"),
License = rule.GetString("license"),
RiskScore = TryRead<int>(rule, "risk_score"),
RiskScore = TryGetInt(rule, "risk_score") ?? 0,
RuleId = rule.GetString("rule_id"),
Severity = rule.GetString("severity"),
Tags = TryGetStringArray(rule, "tags"),
Indices = TryGetStringArray(rule, "index"),
References = TryGetStringArray(rule, "references"),
IndicesFromDateMath = TryGetString(rule, "from"),
Setup = TryGetString(rule, "setup"),
Query = TryGetString(rule, "query"),
Note = TryGetString(rule, "note"),
Name = rule.GetString("name"),
RunsEvery = "?",
MaximumAlertsPerExecution = "?",
Version = "?",
Threats = threats
};
}

private static DetectionRuleThreat[] GetThreats(TomlTable model)
{
if (!model.TryGetValue("threat", out var node) || node is not TomlArray threats)
return [];

var threatsList = new List<DetectionRuleThreat>(threats.ArrayValues.Count);
foreach (var value in threats)
{
if (value is not TomlTable threatTable)
continue;

var framework = threatTable.GetString("framework");
var techniques = ReadTechniques(threatTable);

var tactic = ReadTactic(threatTable);
var threat = new DetectionRuleThreat
{
Framework = framework,
Techniques = techniques.ToArray(),
Tactic = tactic
};
threatsList.Add(threat);
}

return threatsList.ToArray();
}

private static IReadOnlyCollection<DetectionRuleTechnique> ReadTechniques(TomlTable threatTable)
{
var techniquesArray = threatTable.TryGetValue("technique", out var node) && node is TomlArray ta ? ta : null;
if (techniquesArray is null)
return [];
var techniques = new List<DetectionRuleTechnique>(techniquesArray.Count);
foreach (var t in techniquesArray)
{
if (t is not TomlTable techniqueTable)
continue;
var id = techniqueTable.GetString("id");
var name = techniqueTable.GetString("name");
var reference = techniqueTable.GetString("reference");
techniques.Add(new DetectionRuleTechnique
{
Id = id,
Name = name,
Reference = reference,
SubTechniques = ReadSubTechniques(techniqueTable).ToArray()
});
}
return techniques;
}
private static IReadOnlyCollection<DetectionRuleSubTechnique> ReadSubTechniques(TomlTable techniqueTable)
{
var subArray = techniqueTable.TryGetValue("subtechnique", out var node) && node is TomlArray ta ? ta : null;
if (subArray is null)
return [];
var subTechniques = new List<DetectionRuleSubTechnique>(subArray.Count);
foreach (var t in subArray)
{
if (t is not TomlTable subTechniqueTable)
continue;
var id = subTechniqueTable.GetString("id");
var name = subTechniqueTable.GetString("name");
var reference = subTechniqueTable.GetString("reference");
subTechniques.Add(new DetectionRuleSubTechnique
{
Id = id,
Name = name,
Reference = reference
});
}
return subTechniques;
}

private static DetectionRuleTactic ReadTactic(TomlTable threatTable)
{
var tacticTable = threatTable.GetSubTable("tactic");
var id = tacticTable.GetString("id");
var name = tacticTable.GetString("name");
var reference = tacticTable.GetString("reference");
return new DetectionRuleTactic
{
Id = id,
Name = name,
Reference = reference
};
}

Expand All @@ -86,10 +208,6 @@ public static DetectionRule From(IFileInfo source)
private static string? TryGetString(TomlTable table, string key) =>
table.TryGetValue(key, out var node) && node is TomlString t ? t.Value : null;

private static TTarget? TryRead<TTarget>(TomlTable table, string key) =>
table.TryGetValue(key, out var node) && node is TTarget t ? t : default;

private static TTarget Read<TTarget>(TomlTable table, string key) =>
TryRead<TTarget>(table, key) ?? throw new Exception($"Could not find {key} in {table}");

private static int? TryGetInt(TomlTable table, string key) =>
table.TryGetValue(key, out var node) && node is TomlLong t ? (int)t.Value : null;
}
103 changes: 90 additions & 13 deletions src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,47 +27,124 @@ DocumentationSet set

protected override Task<MarkdownDocument> GetMinimalParseDocumentAsync(Cancel ctx)
{
var document = MarkdownParser.MinimalParseStringAsync(Rule?.Note ?? string.Empty, SourceFile, null);
Title = Rule?.Name;
var markdown = GetMarkdown();
var document = MarkdownParser.MinimalParseStringAsync(markdown, SourceFile, null);
return Task.FromResult(document);
}

protected override Task<MarkdownDocument> GetParseDocumentAsync(Cancel ctx)
{
if (Rule == null)
return Task.FromResult(MarkdownParser.ParseStringAsync($"# {Title}", SourceFile, null));
var markdown = GetMarkdown();
var document = MarkdownParser.ParseStringAsync(markdown, SourceFile, null);
return Task.FromResult(document);
}

private string GetMarkdown()
{
if (Rule is null)
return $"# {Title}";
// language=markdown
var markdown = $"""
var markdown =
$"""
# {Rule.Name}

{Rule.Description}

**Rule type**: {Rule.Type}

**Rule indices**: {RenderArray(Rule.Indices)}

**Rule Severity**: {Rule.Severity}

**Risk Score**: {Rule.RiskScore}

**Runs every**: {Rule.RunsEvery}

**Searches indices from**: `{Rule.IndicesFromDateMath}`

**Maximum alerts per execution**: {Rule.MaximumAlertsPerExecution}
**References**: {RenderArray(Rule.References)}

**References**: {RenderArray((Rule.References ?? []).Select(r => $"[{r}]({r})").ToArray())}

**Tags**: {RenderArray(Rule.Tags)}

**Version**: {Rule.Version}

**Rule authors**: {RenderArray(Rule.Authors)}

**Rule license**: {Rule.License}
""";
// language=markdown
if (!string.IsNullOrWhiteSpace(Rule.Setup))
{
markdown +=
$"""

## Investigation guide
{Rule.Setup}
""";
}

{Rule.Note}
// language=markdown
if (!string.IsNullOrWhiteSpace(Rule.Note))
{
markdown +=
$"""

## Rule Query
## Investigation guide

```{Rule.Language ?? Rule.Type}
{Rule.Query}
```
{Rule.Note}
""";
var document = MarkdownParser.ParseStringAsync(markdown, SourceFile, null);
return Task.FromResult(document);
}
// language=markdown
if (!string.IsNullOrWhiteSpace(Rule.Query))
{
markdown +=
$"""

## Rule Query

```{Rule.Language ?? Rule.Type}
{Rule.Query}
```
""";
}

foreach (var threat in Rule.Threats)
{
// language=markdown
markdown +=
$"""

**Framework:** {threat.Framework}

* Tactic:
* Name: {threat.Tactic.Name}
* Id: {threat.Tactic.Id}
* Reference URL: [{threat.Tactic.Reference}]({threat.Tactic.Reference})

""";
foreach (var technique in threat.Techniques)
{
// language=markdown
markdown += TechniqueMarkdown(technique, "Technique");
foreach (var subTechnique in technique.SubTechniques)
markdown += TechniqueMarkdown(subTechnique, "Sub Technique");
}
}
return markdown;
}

private static string TechniqueMarkdown(DetectionRuleSubTechnique technique, string header) =>
$"""

* {header}:
* Name: {technique.Name}
* Id: {technique.Id}
* Reference URL: [{technique.Reference}]({technique.Reference})

""";

private static string RenderArray(string[]? values)
{
if (values == null || values.Length == 0)
Expand Down
Loading
Loading