Skip to content

Commit b8a60d6

Browse files
committed
feat: implement compliance orchestrator backend client and evaluation logic
1 parent bde1081 commit b8a60d6

13 files changed

Lines changed: 550 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/threatwinds/go-sdk/catcher"
8+
"github.com/utmstack/UTMStack/plugins/compliance-orchestrator/client"
9+
)
10+
11+
func waitForBackend(bc *client.BackendClient) error {
12+
maxRetries := 3
13+
retryDelay := 2 * time.Second
14+
15+
for retry := 0; retry < maxRetries; retry++ {
16+
err := bc.HealthCheck(context.Background())
17+
if err == nil {
18+
catcher.Info("Connected to Backend", map[string]any{
19+
"process": "compliance-orchestrator",
20+
})
21+
return nil
22+
}
23+
24+
catcher.Error("Cannot connect to Backend, retrying", err, map[string]any{
25+
"retry": retry + 1,
26+
})
27+
28+
if retry < maxRetries-1 {
29+
time.Sleep(retryDelay)
30+
retryDelay *= 2
31+
} else {
32+
return err
33+
}
34+
}
35+
36+
return nil
37+
}
38+
39+
func waitForOpenSearch() error {
40+
maxRetries := 3
41+
retryDelay := 2 * time.Second
42+
43+
for retry := 0; retry < maxRetries; retry++ {
44+
err := client.ConnectOpenSearch()
45+
if err == nil {
46+
return nil
47+
}
48+
49+
catcher.Error("Cannot connect to OpenSearch, retrying", err, map[string]any{
50+
"retry": retry + 1,
51+
})
52+
53+
if retry < maxRetries-1 {
54+
time.Sleep(retryDelay)
55+
retryDelay *= 2
56+
} else {
57+
return err
58+
}
59+
}
60+
61+
return nil
62+
}
63+
64+
func bootstrap() (*client.BackendClient, error) {
65+
backend := client.NewBackendClient()
66+
67+
if err := waitForBackend(backend); err != nil {
68+
return nil, err
69+
}
70+
71+
if err := waitForOpenSearch(); err != nil {
72+
return nil, err
73+
}
74+
75+
return backend, nil
76+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
"time"
11+
12+
"github.com/threatwinds/go-sdk/catcher"
13+
"github.com/threatwinds/go-sdk/plugins"
14+
"github.com/utmstack/UTMStack/plugins/compliance-orchestrator/models"
15+
)
16+
17+
type BackendClient struct {
18+
baseURL string
19+
internalKey string
20+
httpClient *http.Client
21+
}
22+
23+
func NewBackendClient() *BackendClient {
24+
raw := plugins.PluginCfg("com.utmstack", false).Get("backend").String()
25+
26+
if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
27+
raw = "http://" + raw
28+
}
29+
30+
return &BackendClient{
31+
baseURL: raw,
32+
internalKey: plugins.PluginCfg("com.utmstack", false).Get("internalKey").String(),
33+
httpClient: &http.Client{
34+
Timeout: 30 * time.Second,
35+
},
36+
}
37+
}
38+
39+
func (c *BackendClient) HealthCheck(ctx context.Context) error {
40+
url := fmt.Sprintf("%s/api/ping", c.baseURL)
41+
42+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
43+
if err != nil {
44+
return catcher.Error("failed to create backend ping request", err, nil)
45+
}
46+
47+
resp, err := c.httpClient.Do(req)
48+
if err != nil {
49+
return catcher.Error("backend ping request failed", err, nil)
50+
}
51+
52+
defer func(Body io.ReadCloser) {
53+
err := Body.Close()
54+
if err != nil {
55+
catcher.Error("failed to close backend ping response body", err, nil)
56+
}
57+
}(resp.Body)
58+
59+
if resp.StatusCode != http.StatusOK {
60+
return catcher.Error("backend ping returned non-200", nil, map[string]any{
61+
"status": resp.StatusCode,
62+
})
63+
}
64+
65+
return nil
66+
}
67+
68+
func (c *BackendClient) GetReportConfigs(ctx context.Context) ([]models.ReportConfig, error) {
69+
url := fmt.Sprintf("%s/api/compliance/report-config?page=0&size=1000&sort=id,asc", c.baseURL)
70+
var reports []models.ReportConfig
71+
72+
var body, err = c.GetRequest(ctx, url)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
if err := json.Unmarshal(body, &reports); err != nil {
78+
return nil, err
79+
}
80+
return reports, nil
81+
}
82+
83+
func (c *BackendClient) GetActiveIndexPatterns(ctx context.Context) ([]models.IndexPattern, error) {
84+
url := fmt.Sprintf("%s/api/utm-index-patterns?page=0&size=1000&sort=id,asc&isActive.equals=true", c.baseURL)
85+
var activeIndex []models.IndexPattern
86+
87+
var body, err = c.GetRequest(ctx, url)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
if err := json.Unmarshal(body, &activeIndex); err != nil {
93+
return nil, err
94+
}
95+
return activeIndex, nil
96+
}
97+
98+
func (c *BackendClient) GetRequest(ctx context.Context, url string) ([]byte, error) {
99+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
req.Header.Set("utm-internal-key", c.internalKey)
105+
106+
resp, err := c.httpClient.Do(req)
107+
if err != nil {
108+
return nil, err
109+
}
110+
defer resp.Body.Close()
111+
112+
if resp.StatusCode != http.StatusOK {
113+
return nil, fmt.Errorf("backend returned %d", resp.StatusCode)
114+
}
115+
116+
body, err := io.ReadAll(resp.Body)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
return body, nil
122+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package client
2+
3+
import (
4+
"github.com/threatwinds/go-sdk/catcher"
5+
sdkos "github.com/threatwinds/go-sdk/os"
6+
"github.com/threatwinds/go-sdk/plugins"
7+
)
8+
9+
func ConnectOpenSearch() error {
10+
osUrl := plugins.PluginCfg("org.opensearch", false).Get("opensearch").String()
11+
12+
err := sdkos.Connect([]string{osUrl}, "", "")
13+
if err != nil {
14+
return catcher.Error("failed to connect to OpenSearch", err, map[string]any{
15+
"url": osUrl,
16+
})
17+
}
18+
19+
catcher.Info("Connected to OpenSearch", map[string]any{
20+
"url": osUrl,
21+
})
22+
23+
return nil
24+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package evaluator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/threatwinds/go-sdk/catcher"
8+
"github.com/utmstack/UTMStack/plugins/compliance-orchestrator/client"
9+
"github.com/utmstack/UTMStack/plugins/compliance-orchestrator/models"
10+
)
11+
12+
type Evaluator struct {
13+
backend *client.BackendClient
14+
}
15+
16+
func NewEvaluator(backend *client.BackendClient) *Evaluator {
17+
return &Evaluator{backend: backend}
18+
}
19+
20+
func (e *Evaluator) Evaluate(ctx context.Context, cfg models.ReportConfig) (models.Evaluation, error) {
21+
// 1. Obtener index patterns activos
22+
_, err := e.backend.GetActiveIndexPatterns(ctx)
23+
if err != nil {
24+
return models.Evaluation{}, fmt.Errorf("failed to get index patterns: %w", err)
25+
}
26+
27+
// 2. Evaluar cada QuerySpec
28+
/*var results []models.QueryResult*/
29+
for _, q := range cfg.Queries {
30+
/*qr := e.evaluateQuery(ctx, q, patterns)
31+
results = append(results, qr)*/
32+
catcher.Info("Evaluating query", map[string]any{
33+
"query_id": q.ID,
34+
})
35+
}
36+
37+
/*final := combineResults(cfg, results)
38+
39+
return final, nil*/
40+
41+
return models.Evaluation{}, nil
42+
}
43+
44+
func (e *Evaluator) evaluateQuery(ctx context.Context, q models.QuerySpec, patterns []models.IndexPattern) models.QueryResult {
45+
46+
/*if !patternExists(q.IndexPatternID, patterns) {
47+
return models.QueryResult{
48+
QueryID: int(q.ID),
49+
Status: models.StatusNotApplicable,
50+
Reason: "Index pattern not active",
51+
}
52+
}*/
53+
54+
return models.QueryResult{
55+
QueryID: int(q.ID),
56+
Status: models.StatusCompliant,
57+
Reason: "Query executed successfully (placeholder)",
58+
}
59+
}
60+
61+
func patternExists(pattern int, active []models.IndexPattern) bool {
62+
for _, p := range active {
63+
if p.ID == pattern && p.Active {
64+
return true
65+
}
66+
}
67+
return false
68+
}
69+
70+
func combineResults(cfg models.ReportConfig, results []models.QueryResult) models.Evaluation {
71+
final := models.Evaluation{
72+
ReportID: int(cfg.ID),
73+
Results: results,
74+
}
75+
76+
// Estrategia simple: si alguna query es NON_COMPLIANT → NON_COMPLIANT
77+
for _, r := range results {
78+
if r.Status == models.StatusNonCompliant {
79+
final.Status = models.StatusNonCompliant
80+
return final
81+
}
82+
}
83+
84+
final.Status = models.StatusCompliant
85+
return final
86+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
module github.com/utmstack/UTMStack/plugins/compliance-orchestrator
2+
3+
go 1.25.5
4+
5+
require github.com/threatwinds/go-sdk v1.1.8
6+
7+
require (
8+
cel.dev/expr v0.25.1 // indirect
9+
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
10+
github.com/bytedance/gopkg v0.1.3 // indirect
11+
github.com/bytedance/sonic v1.15.0 // indirect
12+
github.com/bytedance/sonic/loader v0.5.0 // indirect
13+
github.com/cloudwego/base64x v0.1.6 // indirect
14+
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
15+
github.com/gin-contrib/sse v1.1.0 // indirect
16+
github.com/gin-gonic/gin v1.11.0 // indirect
17+
github.com/go-playground/locales v0.14.1 // indirect
18+
github.com/go-playground/universal-translator v0.18.1 // indirect
19+
github.com/go-playground/validator/v10 v10.30.1 // indirect
20+
github.com/goccy/go-json v0.10.5 // indirect
21+
github.com/goccy/go-yaml v1.19.2 // indirect
22+
github.com/google/cel-go v0.26.1 // indirect
23+
github.com/google/uuid v1.6.0 // indirect
24+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
25+
github.com/json-iterator/go v1.1.12 // indirect
26+
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
27+
github.com/leodido/go-urn v1.4.0 // indirect
28+
github.com/mattn/go-isatty v0.0.20 // indirect
29+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
30+
github.com/modern-go/reflect2 v1.0.2 // indirect
31+
github.com/opensearch-project/opensearch-go/v4 v4.6.0 // indirect
32+
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
33+
github.com/quic-go/qpack v0.6.0 // indirect
34+
github.com/quic-go/quic-go v0.59.0 // indirect
35+
github.com/stoewer/go-strcase v1.3.1 // indirect
36+
github.com/tidwall/gjson v1.18.0 // indirect
37+
github.com/tidwall/match v1.2.0 // indirect
38+
github.com/tidwall/pretty v1.2.1 // indirect
39+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
40+
github.com/ugorji/go/codec v1.3.1 // indirect
41+
go.yaml.in/yaml/v2 v2.4.3 // indirect
42+
golang.org/x/arch v0.23.0 // indirect
43+
golang.org/x/crypto v0.47.0 // indirect
44+
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
45+
golang.org/x/net v0.49.0 // indirect
46+
golang.org/x/sys v0.40.0 // indirect
47+
golang.org/x/text v0.33.0 // indirect
48+
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
49+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
50+
google.golang.org/grpc v1.78.0 // indirect
51+
google.golang.org/protobuf v1.36.11 // indirect
52+
gopkg.in/yaml.v3 v3.0.1 // indirect
53+
sigs.k8s.io/yaml v1.6.0 // indirect
54+
)

0 commit comments

Comments
 (0)