update audit view

This commit is contained in:
2026-07-25 15:50:06 -05:00
parent 01a3d1250a
commit 90d5b637ac
8 changed files with 85 additions and 57 deletions
+22 -17
View File
@@ -11,18 +11,19 @@ func TestAuditFiltersFromRequest(t *testing.T) {
t.Parallel()
request := httptest.NewRequest(
"GET",
"/portal?audit_range=7d&audit_event=package_downloaded&audit_actor=taiadmin&audit_hostname=pve01&audit_package=sentinelone-linux&audit_ip=10.10.1.25&audit_details=install-rmm",
"/portal?audit_range=7d&audit_event=package_downloaded&audit_customer=Acme&audit_actor=taiadmin&audit_hostname=pve01&audit_package=sentinelone-linux&audit_ip=10.10.1.25&audit_details=install-rmm",
nil,
)
filters := auditFiltersFromRequest(request)
expected := auditFilters{
TimeRange: "7d",
EventType: "package_downloaded",
Actor: "taiadmin",
Hostname: "pve01",
PackageSlug: "sentinelone-linux",
SourceIP: "10.10.1.25",
Details: "install-rmm",
TimeRange: "7d",
EventType: "package_downloaded",
CustomerLabel: "Acme",
Actor: "taiadmin",
Hostname: "pve01",
PackageSlug: "sentinelone-linux",
SourceIP: "10.10.1.25",
Details: "install-rmm",
}
if !reflect.DeepEqual(filters, expected) {
t.Fatalf("filters = %#v, want %#v", filters, expected)
@@ -50,26 +51,30 @@ func TestHasAuditQuery(t *testing.T) {
func TestAuditQueryUsesPlaceholders(t *testing.T) {
t.Parallel()
filters := auditFilters{
TimeRange: "all",
EventType: "package_downloaded",
Actor: "taiadmin",
Hostname: "pve01",
PackageSlug: "sentinelone-linux",
SourceIP: "10.10.1.25",
Details: "install-rmm",
TimeRange: "all",
EventType: "package_downloaded",
CustomerLabel: "Acme",
Actor: "taiadmin",
Hostname: "pve01",
PackageSlug: "sentinelone-linux",
SourceIP: "10.10.1.25",
Details: "install-rmm",
}
query, arguments := auditQuery(filters, 250)
if strings.Contains(query, filters.Actor) || strings.Contains(query, filters.Hostname) {
if strings.Contains(query, filters.CustomerLabel) ||
strings.Contains(query, filters.Actor) ||
strings.Contains(query, filters.Hostname) {
t.Fatal("filter values must not be interpolated into the SQL query")
}
if strings.Contains(query, "INTERVAL") {
t.Fatal("all-time query must not include a time restriction")
}
if !strings.HasSuffix(query, "ORDER BY created_at DESC LIMIT ?") {
if !strings.HasSuffix(query, "ORDER BY ae.created_at DESC LIMIT ?") {
t.Fatalf("query has unexpected limit: %s", query)
}
expectedArguments := []any{
"package_downloaded",
"Acme",
"taiadmin",
"pve01",
"sentinelone-linux",
+26 -20
View File
@@ -61,6 +61,7 @@ func hasAuditQuery(r *http.Request) bool {
for _, field := range []string{
"audit_range",
"audit_event",
"audit_customer",
"audit_actor",
"audit_hostname",
"audit_package",
@@ -155,13 +156,14 @@ func auditFiltersFromRequest(r *http.Request) auditFilters {
timeRange = "30d"
}
return auditFilters{
TimeRange: timeRange,
EventType: limitedFilter(query.Get("audit_event"), 100),
Actor: limitedFilter(query.Get("audit_actor"), 255),
Hostname: limitedFilter(query.Get("audit_hostname"), 255),
PackageSlug: limitedFilter(query.Get("audit_package"), 100),
SourceIP: limitedFilter(query.Get("audit_ip"), 64),
Details: limitedFilter(query.Get("audit_details"), 255),
TimeRange: timeRange,
EventType: limitedFilter(query.Get("audit_event"), 100),
CustomerLabel: limitedFilter(query.Get("audit_customer"), 255),
Actor: limitedFilter(query.Get("audit_actor"), 255),
Hostname: limitedFilter(query.Get("audit_hostname"), 255),
PackageSlug: limitedFilter(query.Get("audit_package"), 100),
SourceIP: limitedFilter(query.Get("audit_ip"), 64),
Details: limitedFilter(query.Get("audit_details"), 255),
}
}
@@ -198,32 +200,35 @@ func (s *Server) listAuditEventTypes(r *http.Request) ([]string, error) {
}
func auditQuery(filters auditFilters, limit int) (string, []any) {
query := `SELECT event_type, actor, hostname, package_slug, source_ip, details, created_at
FROM audit_events
query := `SELECT ae.event_type, COALESCE(a.customer_label, ''), ae.actor,
ae.hostname, ae.package_slug, ae.source_ip, ae.details, ae.created_at
FROM audit_events ae
LEFT JOIN authorizations a ON a.id = ae.authorization_id
WHERE 1 = 1`
var arguments []any
timeClauses := map[string]string{
"24h": " AND created_at >= UTC_TIMESTAMP(6) - INTERVAL 1 DAY",
"7d": " AND created_at >= UTC_TIMESTAMP(6) - INTERVAL 7 DAY",
"30d": " AND created_at >= UTC_TIMESTAMP(6) - INTERVAL 30 DAY",
"90d": " AND created_at >= UTC_TIMESTAMP(6) - INTERVAL 90 DAY",
"24h": " AND ae.created_at >= UTC_TIMESTAMP(6) - INTERVAL 1 DAY",
"7d": " AND ae.created_at >= UTC_TIMESTAMP(6) - INTERVAL 7 DAY",
"30d": " AND ae.created_at >= UTC_TIMESTAMP(6) - INTERVAL 30 DAY",
"90d": " AND ae.created_at >= UTC_TIMESTAMP(6) - INTERVAL 90 DAY",
}
query += timeClauses[filters.TimeRange]
if filters.EventType != "" {
query += " AND event_type = ?"
query += " AND ae.event_type = ?"
arguments = append(arguments, filters.EventType)
}
containsFilters := []struct {
column string
value string
}{
{"actor", filters.Actor},
{"hostname", filters.Hostname},
{"package_slug", filters.PackageSlug},
{"source_ip", filters.SourceIP},
{"details", filters.Details},
{"COALESCE(a.customer_label, '')", filters.CustomerLabel},
{"ae.actor", filters.Actor},
{"ae.hostname", filters.Hostname},
{"ae.package_slug", filters.PackageSlug},
{"ae.source_ip", filters.SourceIP},
{"ae.details", filters.Details},
}
for _, filter := range containsFilters {
if filter.value == "" {
@@ -232,7 +237,7 @@ func auditQuery(filters auditFilters, limit int) (string, []any) {
query += " AND LOCATE(?, " + filter.column + ") > 0"
arguments = append(arguments, filter.value)
}
query += " ORDER BY created_at DESC LIMIT ?"
query += " ORDER BY ae.created_at DESC LIMIT ?"
arguments = append(arguments, limit)
return query, arguments
}
@@ -254,6 +259,7 @@ func (s *Server) listAuditEvents(r *http.Request, filters auditFilters, limit in
var record auditRecord
if err := rows.Scan(
&record.EventType,
&record.CustomerLabel,
&record.Actor,
&record.Hostname,
&record.PackageSlug,
+16 -14
View File
@@ -69,23 +69,25 @@ type authorizationRecord struct {
}
type auditRecord struct {
EventType string
Actor string
Hostname string
PackageSlug string
SourceIP string
Details string
CreatedAt time.Time
EventType string
CustomerLabel string
Actor string
Hostname string
PackageSlug string
SourceIP string
Details string
CreatedAt time.Time
}
type auditFilters struct {
TimeRange string
EventType string
Actor string
Hostname string
PackageSlug string
SourceIP string
Details string
TimeRange string
EventType string
CustomerLabel string
Actor string
Hostname string
PackageSlug string
SourceIP string
Details string
}
type pageData struct {
+4 -3
View File
@@ -67,9 +67,10 @@ func TestPackageAndAuditTemplatesExecute(t *testing.T) {
data.AuditFilters.TimeRange = "30d"
data.AuditEventTypes = []string{"package_downloaded"}
data.AuditEvents = []auditRecord{{
EventType: "package_downloaded",
Hostname: "pve01",
CreatedAt: time.Now(),
EventType: "package_downloaded",
CustomerLabel: "Acme cluster refresh",
Hostname: "pve01",
CreatedAt: time.Now(),
}}
if err := templates.ExecuteTemplate(io.Discard, "audit.html", data); err != nil {
t.Fatal(err)
+5 -2
View File
@@ -265,11 +265,13 @@ dd { margin: 4px 0 0; overflow-wrap: anywhere; }
.audit-table { display: grid; max-height: 520px; overflow: auto; }
.audit-row {
display: grid;
grid-template-columns: minmax(190px, .8fr) minmax(190px, 1fr) minmax(220px, 1.2fr);
grid-template-columns: minmax(165px, .8fr) minmax(180px, 1fr) minmax(190px, 1fr) minmax(220px, 1.2fr);
gap: 22px;
padding: 15px 28px;
border-bottom: 1px solid var(--line);
}
.audit-header { background: var(--panel-2); }
.audit-header span { color: var(--ink); font-size: .68rem; font-weight: 800; letter-spacing: .06em; text-transform: uppercase; }
.audit-row:last-child { border-bottom: 0; }
.audit-row > div { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
.audit-row span, .audit-row small { color: var(--muted); overflow-wrap: anywhere; }
@@ -302,7 +304,6 @@ dd { margin: 4px 0 0; overflow-wrap: anywhere; }
.hero, .content-grid, .package-layout { grid-template-columns: 1fr; }
.audit-filters { grid-template-columns: 1fr 1fr; }
.audit-row { grid-template-columns: 1fr 1fr; }
.audit-row > div:last-child { grid-column: 1 / -1; }
.policy-card { width: 100%; }
.package-table { border-right: 0; border-bottom: 1px solid var(--line); padding: 0 0 24px; }
}
@@ -319,6 +320,8 @@ dd { margin: 4px 0 0; overflow-wrap: anywhere; }
.page { width: 94vw; padding-top: 34px; }
.field-row, dl { grid-template-columns: 1fr; }
.audit-filters { grid-template-columns: 1fr; }
.audit-row { grid-template-columns: 1fr; }
.audit-header { display: none; }
.code-reveal { align-items: stretch; flex-direction: column; }
.panel-heading-action { align-items: flex-start; flex-direction: column; }
}
+1
View File
@@ -38,6 +38,7 @@
{{end}}
</select>
</label>
<label>Customer / deployment <input name="audit_customer" value="{{.AuditFilters.CustomerLabel}}" placeholder="Acme cluster refresh"></label>
<label>Technician / actor <input name="audit_actor" value="{{.AuditFilters.Actor}}" placeholder="taiadmin"></label>
<label>Hostname <input name="audit_hostname" value="{{.AuditFilters.Hostname}}" placeholder="pve01"></label>
<label>Package ID <input name="audit_package" value="{{.AuditFilters.PackageSlug}}" placeholder="sentinelone-linux"></label>
+9
View File
@@ -25,12 +25,21 @@
{{define "audit-rows"}}
<div class="audit-table">
<div class="audit-row audit-header" aria-hidden="true">
<span>Event</span>
<span>Customer / deployment</span>
<span>Actor / host / package</span>
<span>Source / details</span>
</div>
{{range .AuditEvents}}
<div class="audit-row">
<div>
<strong>{{.EventType}}</strong>
<small>{{formatTime .CreatedAt}}</small>
</div>
<div>
{{if .CustomerLabel}}<span>{{.CustomerLabel}}</span>{{else}}<span></span>{{end}}
</div>
<div>
{{if .Actor}}<span>{{.Actor}}</span>{{end}}
{{if .Hostname}}<span>{{.Hostname}}</span>{{end}}