package main
import (
"bytes"
"flag"
"log"
"os"
"strconv"
"strings"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/util/maps"
"github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/scripts/atomicwrite"
)
var (
auditDocFile string
dryRun bool
generatorPrefix = []byte("")
generatorSuffix = []byte("")
)
/*
*
AuditableResourcesMap is derived from audit.AuditableResources
and has the following structure:
{
friendlyResourceName: {
fieldName1: isTracked,
fieldName2: isTracked,
...
},
...
}
*/
type AuditableResourcesMap map[string]map[string]bool
func main() {
flag.StringVar(&auditDocFile, "audit-doc-file", "docs/admin/security/audit-logs.md", "Path to audit log doc file")
flag.BoolVar(&dryRun, "dry-run", false, "Dry run")
flag.Parse()
auditableResourcesMap := readAuditableResources()
doc, err := readAuditDoc()
if err != nil {
log.Fatal("can't read audit doc: ", err)
}
doc, err = updateAuditDoc(doc, auditableResourcesMap)
if err != nil {
log.Fatal("can't update audit doc: ", err)
}
if dryRun {
log.Println(string(doc))
return
}
err = writeAuditDoc(doc)
if err != nil {
log.Fatal("can't write updated audit doc: ", err)
}
}
// Transforms audit.AuditableResources to AuditableResourcesMap,
// which uses friendlier language.
func readAuditableResources() AuditableResourcesMap {
auditableResourcesMap := make(AuditableResourcesMap)
for resourceName, resourceFields := range audit.AuditableResources {
friendlyResourceName := strings.Split(resourceName, ".")[2]
fieldNameMap := make(map[string]bool)
for fieldName, action := range resourceFields {
fieldNameMap[fieldName] = action != audit.ActionIgnore
auditableResourcesMap[friendlyResourceName] = fieldNameMap
}
}
return auditableResourcesMap
}
func readAuditDoc() ([]byte, error) {
doc, err := os.ReadFile(auditDocFile)
if err != nil {
return nil, err
}
return doc, nil
}
// Writes a markdown table of audit log resources to a buffer
func updateAuditDoc(doc []byte, auditableResourcesMap AuditableResourcesMap) ([]byte, error) {
// We must sort the resources to ensure table ordering
sortedResourceNames := maps.SortedKeys(auditableResourcesMap)
i := bytes.Index(doc, generatorPrefix)
if i < 0 {
return nil, xerrors.New("generator prefix tag not found")
}
tableStartIndex := i + len(generatorPrefix) + 1
j := bytes.Index(doc[tableStartIndex:], generatorSuffix)
if j < 0 {
return nil, xerrors.New("generator suffix tag not found")
}
tableEndIndex := tableStartIndex + j
var buffer bytes.Buffer
_, _ = buffer.Write(doc[:tableStartIndex])
_ = buffer.WriteByte('\n')
_, _ = buffer.WriteString("|Resource||\n")
_, _ = buffer.WriteString("|--|-----------------|\n")
for _, resourceName := range sortedResourceNames {
readableResourceName := resourceName
// AuditableGroup is really a combination of Group and GroupMember resources
// but we use the label 'Group' in our docs to avoid confusion.
if resourceName == "AuditableGroup" {
readableResourceName = "Group"
}
// Create a string of audit actions for each resource
var auditActions []string
for _, action := range audit.AuditActionMap[readableResourceName] {
auditActions = append(auditActions, string(action))
}
auditActionsString := strings.Join(auditActions, ", ")
_, _ = buffer.WriteString("|" + readableResourceName + "
" + auditActionsString + "|
| Field | Tracked |
|---|---|
| " + fieldName + " | " + strconv.FormatBool(isTracked) + " |