mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
## What The audit log resource table header in `docs/admin/security/audit-logs.md` was emitted as `<b>Resource<b>`: a second opening `<b>` instead of a closing `</b>`. Because the bold element never closes, Markdown/HTML renderers can bold content well beyond the header cell. The page is generated (`<!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->`), so the fix belongs in the generator, `scripts/auditdocgen/main.go`, with the doc regenerated from it. ## Changes - `scripts/auditdocgen/main.go`: emit a closing `</b>` instead of a second `<b>` in the table header row. - `docs/admin/security/audit-logs.md`: regenerated with `make docs/admin/security/audit-logs.md`; only the header cell changes. ## Verification <details> <summary>Regenerated doc and local checks</summary> Header cell before (unclosed tag): ```text | <b>Resource<b> | ... ``` Header cell after (balanced tag): ```text | <b>Resource</b> | ... ``` - `make docs/admin/security/audit-logs.md` regenerates the page from the fixed generator and changes only the header cell (single line; the table stays aligned). - Local `make pre-commit` passed with `GEN_SKIP_GOLDEN=1` (this workspace has no Docker daemon for the golden-file gen step, which this change does not touch): `gen`, `fmt`, `lint/go`, `lint/ts`, `lint/markdown`, `lint/typos`, `lint/emdash`, and the slim binary build all green. </details> ## Linear DOCS-580: https://linear.app/codercom/issue/DOCS-580/fix-unclosed-b-tag-in-generated-audit-logs-table-header --- This PR was created using AI (Coder Agents) on behalf of @nickvigilante, who is accountable for its contents. See the [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING).
156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
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("<!-- Code generated by 'make docs/admin/security/audit-logs.md'. DO NOT EDIT -->")
|
|
generatorSuffix = []byte("<!-- End generated by 'make docs/admin/security/audit-logs.md'. -->")
|
|
)
|
|
|
|
/*
|
|
*
|
|
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("|<b>Resource</b>||\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 + "<br><i>" + auditActionsString + "</i>|<table><thead><tr><th>Field</th><th>Tracked</th></tr></thead><tbody>" + "|")
|
|
|
|
// We must sort the field names to ensure sub-table ordering
|
|
sortedFieldNames := maps.SortedKeys(auditableResourcesMap[resourceName])
|
|
|
|
for _, fieldName := range sortedFieldNames {
|
|
isTracked := auditableResourcesMap[resourceName][fieldName]
|
|
_, _ = buffer.WriteString("<tr><td>" + fieldName + "</td><td>" + strconv.FormatBool(isTracked) + "</td></tr>")
|
|
}
|
|
|
|
_, _ = buffer.WriteString("</tbody></table>\n")
|
|
}
|
|
|
|
_, _ = buffer.WriteString("\n")
|
|
_, _ = buffer.Write(doc[tableEndIndex:])
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
func writeAuditDoc(doc []byte) error {
|
|
return atomicwrite.File(auditDocFile, doc)
|
|
}
|