Files
2026-07-13 19:38:35 -05:00

114 lines
3.0 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"sort"
"strings"
)
// Widget represents a ServiceNow Service Portal widget to probe.
type Widget struct {
ID string `json:"id"`
Name string `json:"name"`
}
func abbreviate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n]
}
// DefaultWidgets is the curated list of known ServiceNow widgets that may
// expose data when ACLs are misconfigured. The first entry is
// known to be misconfigured on certain ServiceNow instances.
var DefaultWidgets = []Widget{
{ID: "ff26f2b1db12220099f93691f0b8f5ce", Name: "ticket-attachments"},
{ID: "c6545050ff223100ba13ffffffffffe8", Name: "kb-article-page"},
{ID: "2c429b55cb22020000f8d856634c9cf4", Name: "kb-search"},
{ID: "2f6d5e20cb12020000f8d856634c9c2d", Name: "kb-category-list"},
{ID: "2c16b5c5cb32020000f8d856634c9c48", Name: "sc-category"},
{ID: "1a5af858cb12020000f8d856634c9c30", Name: "simple-list"},
}
func LoadWidgetsFile(path string) ([]Widget, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading widgets file: %w", err)
}
var widgets []Widget
if err := json.Unmarshal(data, &widgets); err != nil {
return nil, fmt.Errorf("parsing widgets file: %w", err)
}
for i, widget := range widgets {
if strings.TrimSpace(widget.ID) == "" {
return nil, fmt.Errorf("widgets file entry %d missing id", i)
}
widgets[i].ID = strings.TrimSpace(widget.ID)
widgets[i].Name = strings.TrimSpace(widget.Name)
if widgets[i].Name == "" {
widgets[i].Name = abbreviate(widgets[i].ID, 8)
}
}
return widgets, nil
}
// DiscoverWidgets queries the sp_widget table via widget-simple-list to enumerate
// all widgets installed on the instance. Returns nil, nil on graceful failure
// (e.g. sp_widget not accessible), so callers can fall back to defaults.
func DiscoverWidgets(ctx context.Context, session *Session, limit int, client *http.Client) ([]Widget, error) {
candidate := TableCandidate{Table: "sp_widget", Field: "id"}
wr, _, err := querySimpleList(ctx, session, candidate, client)
if err != nil {
return nil, nil // graceful: sp_widget not accessible
}
if wr.Result.InvalidToken {
return nil, nil
}
if wr.Result.Data.Count == 0 || len(wr.Result.Data.List) == 0 {
return nil, nil
}
var widgets []Widget
for _, item := range wr.Result.Data.List {
id := nestedDisplayValue(item["display_field"])
if id == "" {
id = valueString(item["sys_id"])
}
if id == "" {
continue
}
widgets = append(widgets, Widget{ID: id, Name: abbreviate(id, 12)})
if limit > 0 && len(widgets) >= limit {
break
}
}
return widgets, nil
}
func MergeWidgets(base, extra []Widget) []Widget {
byID := make(map[string]Widget, len(base)+len(extra))
for _, widget := range append(append([]Widget{}, base...), extra...) {
if widget.ID == "" {
continue
}
byID[widget.ID] = widget
}
out := make([]Widget, 0, len(byID))
for _, widget := range byID {
out = append(out, widget)
}
sort.Slice(out, func(i, j int) bool {
return out[i].ID < out[j].ID
})
return out
}