Skip to content

Commit a7281a8

Browse files
authored
Add extension hook points to all special usecases to inject themselves into docs build process (#666)
* Move detection-rules support behind an extension system * Move EnabledExtensions to Configuration * add untracked files * Add documentation for extensions * Add more extensions points to isolate the DetectionRule extension further * Isolate file scanning in DocumentationSet * Do a ful parse
1 parent 590fcb6 commit a7281a8

20 files changed

+638
-109
lines changed

.editorconfig

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,15 @@ dotnet_diagnostic.IDE0057.severity = none
229229
dotnet_diagnostic.IDE0051.severity = suggestion
230230
dotnet_diagnostic.IDE0059.severity = suggestion
231231

232+
dotnet_diagnostic.CA1859.severity = none
233+
234+
dotnet_diagnostic.IDE0305.severity = none
235+
232236

233237
[DocumentationWebHost.cs]
234238
dotnet_diagnostic.IL3050.severity = none
235239
dotnet_diagnostic.IL2026.severity = none
236240

237-
238-
239241
[tests/**/*.cs]
240242
dotnet_diagnostic.IDE0058.severity = none
241243
dotnet_diagnostic.IDE0022.severity = none

docs/_docset.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ toc:
5858
- file: file-structure.md
5959
- file: attributes.md
6060
- file: navigation.md
61+
- file: extensions.md
6162
- file: page.md
6263
- folder: syntax
6364
children:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
navigation_title: Extensions
3+
---
4+
5+
# Content set extensions.
6+
7+
The documentation engineering team will on occasion built extensions for specific use-cases.
8+
9+
These extension needs to be explicitly opted into since they typically only apply to a few content sets.
10+
11+
12+
## Detection Rules Extensions
13+
14+
For the TRADE team the team built in support to picking up detection rule files from the source and emitting
15+
documentation and navigation for them.
16+
17+
To enable:
18+
19+
```yaml
20+
extensions:
21+
- detection-rules
22+
```
23+
24+
This now allows you to use the special `detection_rules` instruction in the [Table of Contents](navigation.md)
25+
As a means to pick up `toml` files as `children`
26+
27+
```yaml
28+
toc:
29+
- file: index.md
30+
detection_rules: '../rules'
31+
```
32+

src/Elastic.Markdown/BuildContext.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.IO.Abstractions;
66
using Elastic.Markdown.Diagnostics;
7+
using Elastic.Markdown.Extensions;
78
using Elastic.Markdown.IO;
89
using Elastic.Markdown.IO.Configuration;
910
using Elastic.Markdown.IO.Discovery;
@@ -18,6 +19,8 @@ public record BuildContext
1819
public IDirectoryInfo DocumentationSourceDirectory { get; }
1920
public IDirectoryInfo DocumentationOutputDirectory { get; }
2021

22+
public ConfigurationFile Configuration { get; set; }
23+
2124
public IFileInfo ConfigurationPath { get; }
2225

2326
public GitCheckoutInformation Git { get; }
@@ -67,9 +70,8 @@ public BuildContext(DiagnosticsCollector collector, IFileSystem readFileSystem,
6770

6871
Git = GitCheckoutInformation.Create(DocumentationSourceDirectory, ReadFileSystem);
6972
Configuration = new ConfigurationFile(ConfigurationPath, DocumentationSourceDirectory, this);
70-
}
7173

72-
public ConfigurationFile Configuration { get; set; }
74+
}
7375

7476
private (IDirectoryInfo, IFileInfo) FindDocsFolderFromRoot(IDirectoryInfo rootPath)
7577
{
@@ -95,4 +97,5 @@ from folder in knownFolders
9597

9698
return (docsFolder, configurationPath);
9799
}
100+
98101
}

src/Elastic.Markdown/Elastic.Markdown.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151

5252
<ItemGroup>
5353
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
54+
<PackageReference Include="Samboy063.Tomlet" Version="6.0.0" />
5455
<PackageReference Include="SoftCircuits.IniFileParser" Version="2.6.0" />
5556
<PackageReference Include="Markdig" Version="0.39.1" />
5657
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" />
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.IO.Abstractions;
7+
using Tomlet;
8+
using Tomlet.Models;
9+
10+
namespace Elastic.Markdown.Extensions.DetectionRules;
11+
12+
public record DetectionRule
13+
{
14+
public required string Name { get; init; }
15+
16+
public required string[]? Authors { get; init; }
17+
18+
public required string? Note { get; init; }
19+
20+
public required string? Query { get; init; }
21+
22+
public required string[]? Tags { get; init; }
23+
24+
public required string Severity { get; init; }
25+
26+
public required string RuleId { get; init; }
27+
28+
public required int RiskScore { get; init; }
29+
30+
public required string License { get; init; }
31+
32+
public required string Description { get; init; }
33+
public required string Type { get; init; }
34+
public required string? Language { get; init; }
35+
public required string[]? Indices { get; init; }
36+
public required string RunsEvery { get; init; }
37+
public required string? IndicesFromDateMath { get; init; }
38+
public required string MaximumAlertsPerExecution { get; init; }
39+
public required string[]? References { get; init; }
40+
public required string Version { get; init; }
41+
42+
public static DetectionRule From(IFileInfo source)
43+
{
44+
TomlDocument model;
45+
try
46+
{
47+
var sourceText = File.ReadAllText(source.FullName);
48+
model = new TomlParser().Parse(sourceText);
49+
}
50+
catch (Exception e)
51+
{
52+
throw new Exception($"Could not parse toml in: {source.FullName}", e);
53+
}
54+
if (!model.TryGetValue("metadata", out var node) || node is not TomlTable metadata)
55+
throw new Exception($"Could not find metadata section in {source.FullName}");
56+
57+
if (!model.TryGetValue("rule", out node) || node is not TomlTable rule)
58+
throw new Exception($"Could not find rule section in {source.FullName}");
59+
60+
return new DetectionRule
61+
{
62+
Authors = TryGetStringArray(rule, "author"),
63+
Description = rule.GetString("description"),
64+
Type = rule.GetString("type"),
65+
Language = TryGetString(rule, "language"),
66+
License = rule.GetString("license"),
67+
RiskScore = TryRead<int>(rule, "risk_score"),
68+
RuleId = rule.GetString("rule_id"),
69+
Severity = rule.GetString("severity"),
70+
Tags = TryGetStringArray(rule, "tags"),
71+
Indices = TryGetStringArray(rule, "index"),
72+
References = TryGetStringArray(rule, "references"),
73+
IndicesFromDateMath = TryGetString(rule, "from"),
74+
Query = TryGetString(rule, "query"),
75+
Note = TryGetString(rule, "note"),
76+
Name = rule.GetString("name"),
77+
RunsEvery = "?",
78+
MaximumAlertsPerExecution = "?",
79+
Version = "?",
80+
};
81+
}
82+
83+
private static string[]? TryGetStringArray(TomlTable table, string key) =>
84+
table.TryGetValue(key, out var node) && node is TomlArray t ? t.ArrayValues.Select(value => value.StringValue).ToArray() : null;
85+
86+
private static string? TryGetString(TomlTable table, string key) =>
87+
table.TryGetValue(key, out var node) && node is TomlString t ? t.Value : null;
88+
89+
private static TTarget? TryRead<TTarget>(TomlTable table, string key) =>
90+
table.TryGetValue(key, out var node) && node is TTarget t ? t : default;
91+
92+
private static TTarget Read<TTarget>(TomlTable table, string key) =>
93+
TryRead<TTarget>(table, key) ?? throw new Exception($"Could not find {key} in {table}");
94+
95+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.IO.Abstractions;
6+
using Elastic.Markdown.IO;
7+
using Elastic.Markdown.Myst;
8+
using Markdig.Syntax;
9+
10+
namespace Elastic.Markdown.Extensions.DetectionRules;
11+
12+
public record DetectionRuleFile : MarkdownFile
13+
{
14+
public DetectionRule? Rule { get; set; }
15+
16+
public DetectionRuleFile(
17+
IFileInfo sourceFile,
18+
IDirectoryInfo rootPath,
19+
MarkdownParser parser,
20+
BuildContext build,
21+
DocumentationSet set
22+
) : base(sourceFile, rootPath, parser, build, set)
23+
{
24+
}
25+
26+
protected override string RelativePathUrl => RelativePath.AsSpan().TrimStart("../").ToString();
27+
28+
protected override Task<MarkdownDocument> GetMinimalParseDocumentAsync(Cancel ctx)
29+
{
30+
var document = MarkdownParser.MinimalParseStringAsync(Rule?.Note ?? string.Empty, SourceFile, null);
31+
Title = Rule?.Name;
32+
return Task.FromResult(document);
33+
}
34+
35+
protected override Task<MarkdownDocument> GetParseDocumentAsync(Cancel ctx)
36+
{
37+
if (Rule == null)
38+
return Task.FromResult(MarkdownParser.ParseStringAsync($"# {Title}", SourceFile, null));
39+
40+
// language=markdown
41+
var markdown = $"""
42+
# {Rule.Name}
43+
44+
**Rule type**: {Rule.Type}
45+
**Rule indices**: {RenderArray(Rule.Indices)}
46+
**Rule Severity**: {Rule.Severity}
47+
**Risk Score**: {Rule.RiskScore}
48+
**Runs every**: {Rule.RunsEvery}
49+
**Searches indices from**: `{Rule.IndicesFromDateMath}`
50+
**Maximum alerts per execution**: {Rule.MaximumAlertsPerExecution}
51+
**References**: {RenderArray(Rule.References)}
52+
**Tags**: {RenderArray(Rule.Tags)}
53+
**Version**: {Rule.Version}
54+
**Rule authors**: {RenderArray(Rule.Authors)}
55+
**Rule license**: {Rule.License}
56+
57+
## Investigation guide
58+
59+
{Rule.Note}
60+
61+
## Rule Query
62+
63+
```{Rule.Language ?? Rule.Type}
64+
{Rule.Query}
65+
```
66+
""";
67+
var document = MarkdownParser.ParseStringAsync(markdown, SourceFile, null);
68+
return Task.FromResult(document);
69+
}
70+
71+
private static string RenderArray(string[]? values)
72+
{
73+
if (values == null || values.Length == 0)
74+
return string.Empty;
75+
return "\n - " + string.Join("\n - ", values) + "\n";
76+
}
77+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.IO.Abstractions;
6+
using Elastic.Markdown.IO;
7+
using Elastic.Markdown.IO.Configuration;
8+
using Elastic.Markdown.IO.Navigation;
9+
10+
namespace Elastic.Markdown.Extensions.DetectionRules;
11+
12+
public class DetectionRulesDocsBuilderExtension(BuildContext build) : IDocsBuilderExtension
13+
{
14+
private BuildContext Build { get; } = build;
15+
public bool InjectsIntoNavigation(ITocItem tocItem) => tocItem is RulesFolderReference;
16+
17+
public void CreateNavigationItem(
18+
DocumentationGroup? parent,
19+
ITocItem tocItem,
20+
NavigationLookups lookups,
21+
List<DocumentationGroup> groups,
22+
List<INavigationItem> navigationItems,
23+
int depth,
24+
ref int fileIndex,
25+
int index)
26+
{
27+
var detectionRulesFolder = (RulesFolderReference)tocItem;
28+
var children = detectionRulesFolder.Children;
29+
var group = new DocumentationGroup(Build, lookups with { TableOfContents = children }, ref fileIndex, depth + 1)
30+
{
31+
Parent = parent
32+
};
33+
groups.Add(group);
34+
navigationItems.Add(new GroupNavigation(index, depth, group));
35+
}
36+
37+
public void Visit(DocumentationFile file, ITocItem tocItem)
38+
{
39+
// ensure the file has an instance of the rule the reference parsed.
40+
if (file is DetectionRuleFile df && tocItem is RuleReference r)
41+
df.Rule = r.Rule;
42+
}
43+
44+
public DocumentationFile? CreateDocumentationFile(IFileInfo file, IDirectoryInfo sourceDirectory, DocumentationSet documentationSet)
45+
{
46+
if (file.Extension != ".toml")
47+
return null;
48+
49+
return new DetectionRuleFile(file, Build.DocumentationSourceDirectory, documentationSet.MarkdownParser, Build, documentationSet);
50+
}
51+
52+
public bool TryGetDocumentationFileBySlug(DocumentationSet documentationSet, string slug, out DocumentationFile? documentationFile)
53+
{
54+
var tomlFile = $"../{slug}.toml";
55+
return documentationSet.FlatMappedFiles.TryGetValue(tomlFile, out documentationFile);
56+
}
57+
58+
public IReadOnlyCollection<DocumentationFile> ScanDocumentationFiles(
59+
Func<BuildContext, IDirectoryInfo, DocumentationFile[]> scanDocumentationFiles,
60+
Func<IFileInfo, IDirectoryInfo, DocumentationFile> defaultFileHandling
61+
)
62+
{
63+
var rules = Build.Configuration.TableOfContents.OfType<FileReference>().First().Children.OfType<RuleReference>().ToArray();
64+
if (rules.Length == 0)
65+
return [];
66+
67+
var sourcePath = Path.GetFullPath(Path.Combine(Build.DocumentationSourceDirectory.FullName, rules[0].SourceDirectory));
68+
var sourceDirectory = Build.ReadFileSystem.DirectoryInfo.New(sourcePath);
69+
return rules.Select(r =>
70+
{
71+
var file = Build.ReadFileSystem.FileInfo.New(Path.Combine(sourceDirectory.FullName, r.Path));
72+
return defaultFileHandling(file, sourceDirectory);
73+
74+
}).ToArray();
75+
}
76+
77+
public IReadOnlyCollection<ITocItem> CreateTableOfContentItems(
78+
string parentPath,
79+
string detectionRules,
80+
HashSet<string> files
81+
)
82+
{
83+
var detectionRulesFolder = $"{Path.Combine(parentPath, detectionRules)}".TrimStart('/');
84+
var fs = Build.ReadFileSystem;
85+
var sourceDirectory = Build.DocumentationSourceDirectory;
86+
var path = fs.DirectoryInfo.New(fs.Path.GetFullPath(fs.Path.Combine(sourceDirectory.FullName, detectionRulesFolder)));
87+
IReadOnlyCollection<ITocItem> children = path
88+
.EnumerateFiles("*.*", SearchOption.AllDirectories)
89+
.Where(f => !f.Attributes.HasFlag(FileAttributes.Hidden) && !f.Attributes.HasFlag(FileAttributes.System))
90+
.Where(f => !f.Directory!.Attributes.HasFlag(FileAttributes.Hidden) && !f.Directory!.Attributes.HasFlag(FileAttributes.System))
91+
.Where(f => f.Extension is ".md" or ".toml")
92+
.Where(f => f.Name != "README.md")
93+
.Select(f =>
94+
{
95+
var relativePath = Path.GetRelativePath(sourceDirectory.FullName, f.FullName);
96+
if (f.Extension == ".toml")
97+
{
98+
var rule = DetectionRule.From(f);
99+
return new RuleReference(relativePath, detectionRules, true, [], rule);
100+
}
101+
102+
_ = files.Add(relativePath);
103+
return new FileReference(relativePath, true, false, []);
104+
})
105+
.OrderBy(d => d is RuleReference r ? r.Rule.Name : null, StringComparer.OrdinalIgnoreCase)
106+
.ToArray();
107+
108+
//return [new RulesFolderReference(detectionRulesFolder, true, children)];
109+
return children;
110+
}
111+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.IO.Configuration;
6+
7+
namespace Elastic.Markdown.Extensions.DetectionRules;
8+
9+
public record RulesFolderReference(string Path, bool Found, IReadOnlyCollection<ITocItem> Children) : ITocItem;
10+
11+
public record RuleReference(string Path, string SourceDirectory, bool Found, IReadOnlyCollection<ITocItem> Children, DetectionRule Rule)
12+
: FileReference(Path, Found, false, Children);

0 commit comments

Comments
 (0)