mirror of
https://github.com/BishopFox/snowpick.git
synced 2026-11-07 10:26:01 -10:00
114 lines
3.0 KiB
Go
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
|
|
}
|