diff --git a/v2/tools/generate-coverage-matrices.go b/v2/tools/generate-coverage-matrices.go
new file mode 100644
index 00000000..45043cd7
--- /dev/null
+++ b/v2/tools/generate-coverage-matrices.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+
+ "github.com/datadog/stratus-red-team/v2/pkg/stratus"
+)
+
+func GenerateCoverageMatrices(index map[stratus.Platform]map[string][]*stratus.AttackTechnique, docsDirectory string) error {
+ // Process each platform in the index
+ for platform, tacticsMap := range index {
+ // Initialize the HTML content
+ htmlContent := fmt.Sprintf(`
+
+
+
+ %s Coverage Matrix
+
+
+
+
+ Stratus Red Team
+
+ Coverage Matrix for %s
+`, platform, platform)
+
+ // Extract unique tactics
+ tacticsSet := make(map[string]struct{})
+ for tactic := range tacticsMap {
+ tacticsSet[tactic] = struct{}{}
+ }
+ tactics := make([]string, 0, len(tacticsSet))
+ for tactic := range tacticsSet {
+ tactics = append(tactics, tactic)
+ }
+ sort.Strings(tactics)
+
+ // Add header row
+ htmlContent += ""
+ for _, tactic := range tactics {
+ htmlContent += fmt.Sprintf("%s | ", tactic)
+ }
+ htmlContent += "
\n\n"
+
+ rows := make([][]string, 0)
+ maxRows := 0
+
+ // Map tactic to techniques
+ tacticToTechniques := make(map[string][]string)
+ for _, tactic := range tactics {
+ techniques := tacticsMap[tactic]
+ for _, technique := range techniques {
+ tacticToTechniques[tactic] = append(tacticToTechniques[tactic], technique.FriendlyName)
+ }
+ if len(tacticToTechniques[tactic]) > maxRows {
+ maxRows = len(tacticToTechniques[tactic])
+ }
+ }
+
+ // Fill rows with Stratus techniques for each ATT&CK tactic
+ for i := 0; i < maxRows; i++ {
+ row := make([]string, len(tactics))
+ for j, tactic := range tactics {
+ if i < len(tacticToTechniques[tactic]) {
+ row[j] = tacticToTechniques[tactic][i]
+ } else {
+ row[j] = ""
+ }
+ }
+ rows = append(rows, row)
+ }
+
+ // Add rows to the HTML table
+ for _, row := range rows {
+ htmlContent += ""
+ for _, cell := range row {
+ if cell != "" {
+ htmlContent += fmt.Sprintf("%s | ", cell)
+ } else {
+ htmlContent += " | "
+ }
+ }
+ htmlContent += "
\n"
+ }
+
+ // Close table and HTML document
+ htmlContent += `
+
+
+
+`
+ filePath := filepath.Join(docsDirectory, fmt.Sprintf("%s.html", platform))
+ if err := os.WriteFile(filePath, []byte(htmlContent), 0644); err != nil {
+ return fmt.Errorf("failed to write HTML file for platform %s: %w", platform, err)
+ }
+ fmt.Printf("Generated coverage matrix for platform: %s\n", platform)
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/v2/tools/generate-docs.go b/v2/tools/generate-docs.go
index cfbd1fb7..f1c79f77 100644
--- a/v2/tools/generate-docs.go
+++ b/v2/tools/generate-docs.go
@@ -28,6 +28,13 @@ func main() {
os.Exit(1)
}
+ // Generate HTML files for each platform
+ if err := GenerateCoverageMatrices(index, docsDirectory); err != nil {
+ fmt.Fprintln(os.Stderr, "Could not generate coverage matrices")
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
+
// Write a single index file with all techniques. File is enconded in YAML.
yamlIndex := filepath.Join(docsDirectory, "index.yaml")
if err := GenerateYAML(yamlIndex, index); err != nil {