Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+276
View File
@@ -0,0 +1,276 @@
package process
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
)
const ContractVersion = "v1"
type Limits struct {
MaxRows int
MaxPageSize int
}
func (l Limits) withDefaults() Limits {
if l.MaxRows == 0 {
l.MaxRows = 1000
}
if l.MaxPageSize == 0 {
l.MaxPageSize = 100
}
return l
}
func (l Limits) Validate() error {
if l.MaxRows < 1 || l.MaxRows > 5000 || l.MaxPageSize < 1 || l.MaxPageSize > 500 {
return errors.New("process limits are outside safe bounds")
}
return nil
}
type Source struct {
ID string `json:"id"`
Type string `json:"type"`
CapabilityVersion string `json:"capabilityVersion"`
ObservedAt time.Time `json:"observedAt"`
ReceivedAt time.Time `json:"receivedAt"`
State string `json:"state"`
Reason string `json:"reason,omitempty"`
}
type RawProcess struct {
PID int `json:"pid"`
Name string `json:"name"`
State string `json:"state"`
RuntimeSeconds float64 `json:"runtimeSeconds"`
CPUPercent float64 `json:"cpuPercent"`
MemoryBytes uint64 `json:"memoryBytes"`
ContainerID string `json:"containerId,omitempty"`
ContainerName string `json:"containerName,omitempty"`
}
type Process struct {
PID int `json:"pid"`
Name string `json:"name"`
State string `json:"state"`
RuntimeSeconds float64 `json:"runtimeSeconds"`
CPUPercent float64 `json:"cpuPercent"`
MemoryBytes uint64 `json:"memoryBytes"`
ContainerID string `json:"containerId,omitempty"`
ContainerName string `json:"containerName,omitempty"`
}
type RawSnapshot struct {
Source Source `json:"source"`
Processes []RawProcess `json:"processes"`
ObservedAt time.Time `json:"observedAt"`
ReceivedAt time.Time `json:"receivedAt"`
}
type Snapshot struct {
ContractVersion string `json:"contractVersion"`
Source Source `json:"source"`
Processes []Process `json:"processes"`
Total int `json:"total"`
NextCursor string `json:"nextCursor,omitempty"`
}
type Provider interface {
Snapshot(context.Context) (RawSnapshot, error)
}
type Adapter struct {
Source Provider
Limits Limits
Now func() time.Time
}
func (a Adapter) Snapshot(ctx context.Context) (Snapshot, error) {
if err := ctx.Err(); err != nil {
return Snapshot{}, err
}
if a.Source == nil {
return UnknownSnapshot(time.Now().UTC(), "process", "agent", "source_unavailable"), nil
}
raw, err := a.Source.Snapshot(ctx)
if err != nil {
return Snapshot{}, err
}
now := time.Now().UTC()
if a.Now != nil {
now = a.Now()
}
return Normalize(raw, now, a.Limits)
}
func UnknownSnapshot(now time.Time, sourceID, sourceType, reason string) Snapshot {
if now.IsZero() {
now = time.Now().UTC()
}
if reason == "" {
reason = "source_unavailable"
}
return Snapshot{ContractVersion: ContractVersion, Source: Source{ID: sourceID, Type: sourceType, CapabilityVersion: ContractVersion, ReceivedAt: now, State: "unknown", Reason: reason}, Processes: []Process{}, Total: 0}
}
func Normalize(raw RawSnapshot, now time.Time, limits Limits) (Snapshot, error) {
limits = limits.withDefaults()
if err := limits.Validate(); err != nil {
return Snapshot{}, err
}
if now.IsZero() {
now = time.Now().UTC()
}
if raw.ReceivedAt.IsZero() {
raw.ReceivedAt = now
}
if raw.ObservedAt.IsZero() {
raw.ObservedAt = raw.ReceivedAt
}
if raw.ObservedAt.After(now.Add(time.Minute)) {
return Snapshot{}, errors.New("process observation is materially in the future")
}
if len(raw.Processes) > limits.MaxRows {
return Snapshot{}, errors.New("process rows exceed bounds")
}
source := raw.Source
if source.ID == "" {
source.ID = "process"
}
if source.Type == "" {
source.Type = "agent"
}
if source.CapabilityVersion == "" {
source.CapabilityVersion = ContractVersion
}
source.ObservedAt = raw.ObservedAt.UTC()
source.ReceivedAt = raw.ReceivedAt.UTC()
source.State = "healthy"
processes := make([]Process, 0, len(raw.Processes))
for _, item := range raw.Processes {
if item.PID < 1 || item.PID > 2147483647 || strings.TrimSpace(item.Name) == "" || len(item.Name) > 512 || strings.ContainsRune(item.Name, '\x00') {
return Snapshot{}, errors.New("invalid process identity")
}
if item.RuntimeSeconds < 0 || item.RuntimeSeconds > 100*365*24*60*60 || math.IsNaN(item.RuntimeSeconds) || math.IsInf(item.RuntimeSeconds, 0) || item.CPUPercent < 0 || item.CPUPercent > 10000 || math.IsNaN(item.CPUPercent) || math.IsInf(item.CPUPercent, 0) {
return Snapshot{}, errors.New("invalid process metrics")
}
name := privacyName(item.Name)
if name == "" {
return Snapshot{}, errors.New("process name is empty after privacy normalization")
}
processes = append(processes, Process{PID: item.PID, Name: name, State: boundedState(item.State), RuntimeSeconds: item.RuntimeSeconds, CPUPercent: item.CPUPercent, MemoryBytes: item.MemoryBytes, ContainerID: boundedOptional(item.ContainerID, 128), ContainerName: boundedOptional(item.ContainerName, 255)})
}
return Snapshot{ContractVersion: ContractVersion, Source: source, Processes: processes, Total: len(processes)}, nil
}
type SortMode string
const (
SortCPU SortMode = "cpu"
SortMemory SortMode = "memory"
)
func Page(snapshot Snapshot, mode SortMode, limit int, after string, limits Limits) (Snapshot, error) {
return FilteredPage(snapshot, mode, limit, after, limits, "", "")
}
func FilteredPage(snapshot Snapshot, mode SortMode, limit int, after string, limits Limits, query, containerName string) (Snapshot, error) {
limits = limits.withDefaults()
if err := limits.Validate(); err != nil {
return Snapshot{}, err
}
if limit < 1 || limit > limits.MaxPageSize {
return Snapshot{}, errors.New("process page limit is outside bounds")
}
if mode != SortCPU && mode != SortMemory {
return Snapshot{}, errors.New("process sort must be cpu or memory")
}
query, containerName = strings.ToLower(strings.TrimSpace(query)), strings.ToLower(strings.TrimSpace(containerName))
if len(query) > 100 || len(containerName) > 100 {
return Snapshot{}, errors.New("process filters are invalid")
}
items := make([]Process, 0, len(snapshot.Processes))
for _, item := range snapshot.Processes {
if query != "" && !strings.Contains(strings.ToLower(item.Name), query) {
continue
}
if containerName != "" && !strings.Contains(strings.ToLower(item.ContainerName), containerName) {
continue
}
items = append(items, item)
}
start := 0
if after != "" {
parsed, err := strconv.Atoi(after)
if err != nil || parsed < 0 || parsed > len(items) {
return Snapshot{}, errors.New("invalid process cursor")
}
start = parsed
}
sort.SliceStable(items, func(i, j int) bool {
if mode == SortMemory && items[i].MemoryBytes != items[j].MemoryBytes {
return items[i].MemoryBytes > items[j].MemoryBytes
}
if mode == SortCPU && items[i].CPUPercent != items[j].CPUPercent {
return items[i].CPUPercent > items[j].CPUPercent
}
if items[i].PID != items[j].PID {
return items[i].PID < items[j].PID
}
return items[i].Name < items[j].Name
})
if start > len(items) {
start = len(items)
}
end := start + limit
if end > len(items) {
end = len(items)
}
result := snapshot
result.Processes = items[start:end]
result.Total = len(items)
result.NextCursor = ""
if end < len(items) {
result.NextCursor = strconv.Itoa(end)
}
return result, nil
}
func privacyName(value string) string {
value = strings.TrimSpace(value)
if index := strings.IndexAny(value, " "+string(rune(9))); index >= 0 {
value = value[:index]
}
if index := strings.LastIndexAny(value, "/\\"); index >= 0 {
value = value[index+1:]
}
if len(value) > 255 {
value = value[:255]
}
return value
}
func boundedState(value string) string {
value = strings.TrimSpace(value)
if len(value) > 40 {
return value[:40]
}
if value == "" {
return "unknown"
}
return value
}
func boundedOptional(value string, max int) string {
value = strings.TrimSpace(value)
if len(value) > max {
return value[:max]
}
return value
}
func (s Snapshot) String() string { return fmt.Sprintf("%s/%d", s.Source.ID, s.Total) }
+89
View File
@@ -0,0 +1,89 @@
package process
import (
"context"
"errors"
"testing"
"time"
)
func processFixture(now time.Time) RawSnapshot {
return RawSnapshot{Source: Source{ID: "agent-1", Type: "agent"}, Processes: []RawProcess{{PID: 2, Name: "/usr/bin/worker --secret-token=redacted", State: "running", RuntimeSeconds: 20, CPUPercent: 40, MemoryBytes: 200}, {PID: 1, Name: "init", State: "sleeping", RuntimeSeconds: 100, CPUPercent: 40, MemoryBytes: 500}}, ObservedAt: now, ReceivedAt: now}
}
func TestNormalizeMinimizesSensitiveProcessNameAndPreservesMetrics(t *testing.T) {
now := time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC)
snapshot, err := Normalize(processFixture(now), now, Limits{})
if err != nil {
t.Fatal(err)
}
if snapshot.Processes[0].Name != "worker" || snapshot.Processes[0].MemoryBytes != 200 || snapshot.Processes[0].CPUPercent != 40 {
t.Fatalf("unexpected process normalization: %+v", snapshot.Processes[0])
}
if snapshot.Processes[0].Name == "init" {
t.Fatal("unexpected fixture ordering assumption")
}
}
func TestPageIsBoundedAndDeterministic(t *testing.T) {
now := time.Now().UTC()
snapshot, err := Normalize(processFixture(now), now, Limits{})
if err != nil {
t.Fatal(err)
}
page, err := Page(snapshot, SortCPU, 1, "", Limits{})
if err != nil {
t.Fatal(err)
}
if len(page.Processes) != 1 || page.Processes[0].PID != 1 || page.NextCursor != "1" || page.Total != 2 {
t.Fatalf("page=%+v", page)
}
next, err := Page(snapshot, SortCPU, 1, page.NextCursor, Limits{})
if err != nil {
t.Fatal(err)
}
if len(next.Processes) != 1 || next.Processes[0].PID != 2 || next.NextCursor != "" {
t.Fatalf("next=%+v", next)
}
if _, err := Page(snapshot, SortCPU, 101, "", Limits{MaxPageSize: 100}); err == nil {
t.Fatal("unbounded page accepted")
}
}
func TestFilteredPageFiltersBeforePagination(t *testing.T) {
snapshot := Snapshot{Processes: []Process{{PID: 1, Name: "postgres", ContainerName: "db", CPUPercent: 2}, {PID: 2, Name: "worker", ContainerName: "pulse", CPUPercent: 5}, {PID: 3, Name: "api", ContainerName: "pulse", CPUPercent: 4}}}
page, err := FilteredPage(snapshot, SortCPU, 1, "", Limits{}, "", "pulse")
if err != nil || page.Total != 2 || page.Processes[0].PID != 2 || page.NextCursor != "1" {
t.Fatalf("filtered page=%+v err=%v", page, err)
}
}
func TestNormalizeRejectsUnboundedRowsAndDoesNotExposeControlFields(t *testing.T) {
now := time.Now().UTC()
raw := processFixture(now)
raw.Processes = make([]RawProcess, 1001)
if _, err := Normalize(raw, now, Limits{}); err == nil {
t.Fatal("unbounded rows accepted")
}
if _, ok := interface{}(Process{}).(interface{ Kill() }); ok {
t.Fatal("process model exposes kill capability")
}
}
type source struct{ value RawSnapshot }
func (s source) Snapshot(ctx context.Context) (RawSnapshot, error) {
if err := ctx.Err(); err != nil {
return RawSnapshot{}, err
}
return s.value, nil
}
func TestAdapterHonorsCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := (Adapter{Source: source{value: processFixture(time.Now().UTC())}}).Snapshot(ctx)
if !errors.Is(err, context.Canceled) {
t.Fatalf("err=%v", err)
}
}