Skip to content

Commit 93bda8a

Browse files
committed
Normalize CLI JSON parsing across handlers
parseCLIJSONToBytes / parseCLIJSONOutput helpers and rename firstJSONStartIndex to firstCLIJSONStartIndex; update multiple API handlers to normalize and parse cscli CLI JSON output before decoding. Replace direct json.Unmarshal([]byte(output)) use with normalized bytes and add improved error handling, logging, and appropriate HTTP responses where parsing/normalization fails. Updated related tests and minor cleanup across alerts_inspect, allowlists, bouncers, common, dashboard, dashboard_analysis, health_diagnostics, hub, hub_parse_test, ip, scenarios and simulation handlers.
1 parent c457f4a commit 93bda8a

12 files changed

Lines changed: 168 additions & 52 deletions

internal/api/handlers/alerts_inspect.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,9 @@ func InspectAlert(dockerClient *docker.Client, cfg *config.Config) gin.HandlerFu
102102

103103
// Parse the JSON output
104104
var alertData interface{}
105-
if err := json.Unmarshal([]byte(output), &alertData); err != nil {
106-
logger.Warn("Failed to parse alert inspect JSON", "alertID", alertID, "error", err)
105+
dataBytes, parseErr := parseCLIJSONToBytes(output)
106+
if parseErr != nil || json.Unmarshal(dataBytes, &alertData) != nil {
107+
logger.Warn("Failed to parse alert inspect JSON", "alertID", alertID, "error", parseErr)
107108
// Return raw output if parsing fails
108109
c.JSON(http.StatusOK, models.Response{
109110
Success: true,

internal/api/handlers/allowlists.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,17 @@ func ListAllowlists(dockerClient *docker.Client, cfg *config.Config) gin.Handler
4444
return
4545
}
4646

47-
// Parse allowlists using jsonparser
47+
// Parse allowlists using normalized CLI JSON output.
4848
var allowlists []models.Allowlist
49-
dataBytes := []byte(output)
49+
dataBytes, parseErr := parseCLIJSONToBytes(output)
50+
if parseErr != nil {
51+
logger.Error("Failed to normalize allowlists JSON", "error", parseErr, "output", output)
52+
c.JSON(http.StatusInternalServerError, models.Response{
53+
Success: false,
54+
Error: fmt.Sprintf("Failed to parse allowlists: %v", parseErr),
55+
})
56+
return
57+
}
5058

5159
_, err = jsonparser.ArrayEach(dataBytes, func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
5260
var allowlist models.Allowlist
@@ -111,7 +119,6 @@ func ListAllowlists(dockerClient *docker.Client, cfg *config.Config) gin.Handler
111119
}
112120
}
113121

114-
115122
// CreateAllowlist creates a new CrowdSec allowlist
116123
func CreateAllowlist(dockerClient *docker.Client, cfg *config.Config) gin.HandlerFunc {
117124
return func(c *gin.Context) {
@@ -167,8 +174,16 @@ func InspectAllowlist(dockerClient *docker.Client, cfg *config.Config) gin.Handl
167174
return
168175
}
169176

170-
// Parse response using jsonparser
171-
dataBytes := []byte(output)
177+
// Parse response using normalized CLI JSON output.
178+
dataBytes, parseErr := parseCLIJSONToBytes(output)
179+
if parseErr != nil {
180+
logger.Error("Failed to normalize allowlist inspect JSON", "name", name, "error", parseErr)
181+
c.JSON(http.StatusInternalServerError, models.Response{
182+
Success: false,
183+
Error: fmt.Sprintf("Failed to parse allowlist response: %v", parseErr),
184+
})
185+
return
186+
}
172187
var response models.AllowlistInspectResponse
173188

174189
// Extract top-level fields
@@ -226,7 +241,6 @@ func InspectAllowlist(dockerClient *docker.Client, cfg *config.Config) gin.Handl
226241
}
227242
}
228243

229-
230244
// AddAllowlistEntries adds entries to an allowlist
231245
func AddAllowlistEntries(dockerClient *docker.Client, cfg *config.Config) gin.HandlerFunc {
232246
return func(c *gin.Context) {
@@ -329,4 +343,3 @@ func DeleteAllowlist(dockerClient *docker.Client, cfg *config.Config) gin.Handle
329343
})
330344
}
331345
}
332-

internal/api/handlers/bouncers.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,22 @@ func GetBouncers(dockerClient *docker.Client, cfg *config.Config) gin.HandlerFun
3333
return
3434
}
3535

36-
// Parse the JSON to ensure it's valid and return as structured data
37-
var bouncers []models.Bouncer
38-
if err := json.Unmarshal([]byte(output), &bouncers); err != nil {
39-
// If JSON parsing fails, log details and return error
36+
// Parse and normalize CLI JSON output first, then decode typed payload.
37+
dataBytes, parseErr := parseCLIJSONToBytes(output)
38+
if parseErr != nil {
4039
logger.Warn("Failed to parse bouncers JSON",
41-
"error", err,
40+
"error", parseErr,
4241
"output_length", len(output),
4342
"output_preview", truncateString(output, 100))
43+
c.JSON(http.StatusInternalServerError, models.Response{
44+
Success: false,
45+
Error: fmt.Sprintf("Failed to parse bouncers JSON: %v", parseErr),
46+
})
47+
return
48+
}
49+
50+
var bouncers []models.Bouncer
51+
if err := json.Unmarshal(dataBytes, &bouncers); err != nil {
4452
c.JSON(http.StatusInternalServerError, models.Response{
4553
Success: false,
4654
Error: fmt.Sprintf("Failed to parse bouncers JSON: %v", err),
@@ -179,8 +187,17 @@ func DeleteBouncer(dockerClient *docker.Client, cfg *config.Config) gin.HandlerF
179187
checkCmd := []string{"cscli", "bouncers", "list", "-o", "json"}
180188
checkOutput, checkErr := dockerClient.ExecCommand(cfg.CrowdsecContainerName, checkCmd)
181189
if checkErr == nil {
190+
dataBytes, parseErr := parseCLIJSONToBytes(checkOutput)
191+
if parseErr != nil {
192+
logger.Warn("Failed to parse bouncer verification JSON", "error", parseErr, "output_preview", truncateString(checkOutput, 100))
193+
}
182194
var bouncers []models.Bouncer
183-
if jsonErr := json.Unmarshal([]byte(checkOutput), &bouncers); jsonErr == nil {
195+
if parseErr == nil {
196+
if jsonErr := json.Unmarshal(dataBytes, &bouncers); jsonErr != nil {
197+
logger.Warn("Failed to decode bouncer verification JSON", "error", jsonErr)
198+
}
199+
}
200+
if parseErr == nil {
184201
for _, b := range bouncers {
185202
if b.Name == name {
186203
// Bouncer still exists!

internal/api/handlers/common.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ func truncateString(s string, maxLen int) string {
3838
return s[:maxLen] + "... (truncated)"
3939
}
4040

41+
func parseCLIJSONToBytes(output string) ([]byte, error) {
42+
parsed, err := parseCLIJSONOutput(output)
43+
if err != nil {
44+
return nil, err
45+
}
46+
data, err := json.Marshal(parsed)
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to marshal parsed CLI JSON: %w", err)
49+
}
50+
return data, nil
51+
}
52+
4153
// Helper functions for safe type conversion from map[string]interface{}
4254
func getString(m map[string]interface{}, key string) string {
4355
if v, ok := m[key]; ok {
@@ -126,8 +138,15 @@ func GetConsoleStatusHelper(dockerClient interface {
126138
logger.Info("Console status raw output", "output", output)
127139

128140
var status models.ConsoleStatus
129-
if err := json.Unmarshal([]byte(output), &status); err != nil {
130-
logger.Warn("Failed to parse console status JSON, attempting fallback", "error", err)
141+
dataBytes, parseErr := parseCLIJSONToBytes(output)
142+
if parseErr == nil {
143+
if err := json.Unmarshal(dataBytes, &status); err != nil {
144+
parseErr = err
145+
}
146+
}
147+
148+
if parseErr != nil {
149+
logger.Warn("Failed to parse console status JSON, attempting fallback", "error", parseErr)
131150

132151
// Fallback to simple string check if JSON parsing fails
133152
// This handles cases where older versions might not output valid JSON or other issues

internal/api/handlers/dashboard.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,17 @@ func GetDecisions(dockerClient *docker.Client, cfg *config.Config, ttlCache ...*
6565
return
6666
}
6767

68-
// Parse alerts using jsonparser
68+
// Parse alerts using normalized CLI JSON output.
6969
var decisions []models.Decision
70-
dataBytes := []byte(output)
70+
dataBytes, parseErr := parseCLIJSONToBytes(output)
71+
if parseErr != nil {
72+
logger.Error("Failed to normalize decisions JSON", "error", parseErr, "output_preview", truncateString(output, 200))
73+
c.JSON(http.StatusInternalServerError, models.Response{
74+
Success: false,
75+
Error: fmt.Sprintf("Failed to parse decisions JSON: %v", parseErr),
76+
})
77+
return
78+
}
7179

7280
_, err = jsonparser.ArrayEach(dataBytes, func(alertValue []byte, alertType jsonparser.ValueType, alertOffset int, alertErr error) {
7381
// Get alert's created_at for fallback
@@ -188,7 +196,16 @@ func GetMetrics(dockerClient *docker.Client, cfg *config.Config, ttlCache ...*ca
188196
}
189197

190198
var metrics interface{}
191-
if err := json.Unmarshal([]byte(output), &metrics); err != nil {
199+
dataBytes, parseErr := parseCLIJSONToBytes(output)
200+
if parseErr != nil {
201+
logger.Warn("Failed to normalize metrics JSON", "error", parseErr)
202+
c.JSON(http.StatusInternalServerError, models.Response{
203+
Success: false,
204+
Error: fmt.Sprintf("Failed to parse metrics JSON: %v", parseErr),
205+
})
206+
return
207+
}
208+
if err := json.Unmarshal(dataBytes, &metrics); err != nil {
192209
logger.Warn("Failed to parse metrics JSON", "error", err)
193210
c.JSON(http.StatusInternalServerError, models.Response{
194211
Success: false,

internal/api/handlers/dashboard_analysis.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,17 @@ func GetDecisionsAnalysis(dockerClient *docker.Client, cfg *config.Config) gin.H
7171
return
7272
}
7373

74-
// Parse alerts using jsonparser
74+
// Parse alerts using normalized CLI JSON output.
7575
var decisions []models.Decision
76-
dataBytes := []byte(output)
76+
dataBytes, parseErr := parseCLIJSONToBytes(output)
77+
if parseErr != nil {
78+
logger.Error("Failed to normalize decisions analysis JSON", "error", parseErr, "output_preview", truncateString(output, 200))
79+
c.JSON(http.StatusInternalServerError, models.Response{
80+
Success: false,
81+
Error: fmt.Sprintf("Failed to parse decisions JSON: %v", parseErr),
82+
})
83+
return
84+
}
7785

7886
_, err = jsonparser.ArrayEach(dataBytes, func(alertValue []byte, alertType jsonparser.ValueType, alertOffset int, alertErr error) {
7987
// Get alert's created_at for fallback
@@ -203,11 +211,27 @@ func GetAlertsAnalysis(dockerClient *docker.Client, cfg *config.Config, ttlCache
203211
}
204212

205213
// Parse JSON
214+
dataBytes, parseErr := parseCLIJSONToBytes(output)
215+
if parseErr != nil {
216+
if output == "null" || output == "" {
217+
c.JSON(http.StatusOK, models.Response{
218+
Success: true,
219+
Data: gin.H{"alerts": []interface{}{}, "count": 0},
220+
})
221+
return
222+
}
223+
logger.Warn("Failed to normalize alerts JSON", "error", parseErr)
224+
c.JSON(http.StatusInternalServerError, models.Response{
225+
Success: false,
226+
Error: fmt.Sprintf("Failed to parse alerts: %v", parseErr),
227+
})
228+
return
229+
}
206230
var alerts []interface{}
207-
if err := json.Unmarshal([]byte(output), &alerts); err != nil {
231+
if err := json.Unmarshal(dataBytes, &alerts); err != nil {
208232
// If unmarshaling as list fails, try as a single object (behavior for inspect command)
209233
var singleAlert interface{}
210-
if errSingle := json.Unmarshal([]byte(output), &singleAlert); errSingle == nil {
234+
if errSingle := json.Unmarshal(dataBytes, &singleAlert); errSingle == nil {
211235
alerts = []interface{}{singleAlert}
212236
} else {
213237
if output == "null" || output == "" {

internal/api/handlers/health_diagnostics.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ import (
1818
// parseBouncersJSON parses bouncers JSON output and returns bouncer list with status
1919
func parseBouncersJSON(bouncerOutput string, computeStatus bool) ([]models.Bouncer, error) {
2020
var bouncers []models.Bouncer
21-
dataBytes := []byte(bouncerOutput)
21+
dataBytes, err := parseCLIJSONToBytes(bouncerOutput)
22+
if err != nil {
23+
return bouncers, err
24+
}
2225

23-
_, err := jsonparser.ArrayEach(dataBytes, func(value []byte, dataType jsonparser.ValueType, offset int, parseErr error) {
26+
_, err = jsonparser.ArrayEach(dataBytes, func(value []byte, dataType jsonparser.ValueType, offset int, parseErr error) {
2427
var bouncer models.Bouncer
2528

2629
if name, err := jsonparser.GetString(value, "name"); err == nil {
@@ -118,7 +121,7 @@ func checkMetricsHealth(dockerClient *docker.Client, containerName string, metri
118121
}
119122

120123
var metricsData map[string]interface{}
121-
if err := json.Unmarshal([]byte(metricsOutput), &metricsData); err == nil {
124+
if dataBytes, parseErr := parseCLIJSONToBytes(metricsOutput); parseErr == nil && json.Unmarshal(dataBytes, &metricsData) == nil {
122125
return models.HealthCheckItem{
123126
Status: "healthy",
124127
Message: "Metrics endpoint is accessible",
@@ -175,7 +178,15 @@ func parseDiagnosticDecisions(decisionOutput string) []models.Decision {
175178
var decisions []models.Decision
176179

177180
var rawDecisions []map[string]interface{}
178-
if err := json.Unmarshal([]byte(decisionOutput), &rawDecisions); err != nil {
181+
dataBytes, parseErr := parseCLIJSONToBytes(decisionOutput)
182+
if parseErr != nil {
183+
logger.Warn("Failed to normalize decisions JSON",
184+
"error", parseErr,
185+
"output_length", len(decisionOutput),
186+
"output_preview", truncateString(decisionOutput, 100))
187+
return decisions
188+
}
189+
if err := json.Unmarshal(dataBytes, &rawDecisions); err != nil {
179190
logger.Warn("Failed to parse decisions JSON",
180191
"error", err,
181192
"output_length", len(decisionOutput),

internal/api/handlers/hub.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ func upsertHubPreference(db *database.Database, category, mode, defaultPath, las
195195
}
196196
}
197197

198-
func firstJSONStartIndex(output string) int {
198+
func firstCLIJSONStartIndex(output string) int {
199199
objectStart := strings.Index(output, "{")
200200
arrayStart := strings.Index(output, "[")
201201

@@ -211,10 +211,10 @@ func firstJSONStartIndex(output string) int {
211211
}
212212
}
213213

214-
// parseHubJSONOutput extracts the first JSON value from mixed CLI output.
214+
// parseCLIJSONOutput extracts the first JSON value from mixed CLI output.
215215
// cscli may print informational preamble/trailing lines around the JSON body.
216-
func parseHubJSONOutput(output string) (interface{}, error) {
217-
start := firstJSONStartIndex(output)
216+
func parseCLIJSONOutput(output string) (interface{}, error) {
217+
start := firstCLIJSONStartIndex(output)
218218
if start < 0 {
219219
return nil, fmt.Errorf("no JSON payload found")
220220
}
@@ -256,7 +256,7 @@ func ListHubItems(dockerClient *docker.Client, cfg *config.Config) gin.HandlerFu
256256
return
257257
}
258258

259-
parsed, err := parseHubJSONOutput(output)
259+
parsed, err := parseCLIJSONOutput(output)
260260
if err != nil {
261261
logger.Warn("Failed to parse hub list JSON", "error", err, "output_preview", truncateString(output, 200))
262262
c.JSON(http.StatusOK, models.Response{
@@ -292,7 +292,7 @@ func ListHubItemsByCategory(dockerClient *docker.Client, cfg *config.Config) gin
292292
return
293293
}
294294

295-
parsed, err := parseHubJSONOutput(output)
295+
parsed, err := parseCLIJSONOutput(output)
296296
if err != nil {
297297
c.JSON(http.StatusOK, models.Response{Success: true, Data: gin.H{"category": spec, "raw_output": output}})
298298
return

internal/api/handlers/hub_parse_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"testing"
55
)
66

7-
func TestParseHubJSONOutput(t *testing.T) {
7+
func TestParseCLIJSONOutput(t *testing.T) {
88
tests := []struct {
99
name string
1010
input string
@@ -63,7 +63,7 @@ func TestParseHubJSONOutput(t *testing.T) {
6363

6464
for _, tt := range tests {
6565
t.Run(tt.name, func(t *testing.T) {
66-
parsed, err := parseHubJSONOutput(tt.input)
66+
parsed, err := parseCLIJSONOutput(tt.input)
6767
if tt.wantError {
6868
if err == nil {
6969
t.Fatalf("expected error, got parsed=%#v", parsed)
@@ -81,7 +81,7 @@ func TestParseHubJSONOutput(t *testing.T) {
8181
}
8282
}
8383

84-
func TestFirstJSONStartIndex(t *testing.T) {
84+
func TestFirstCLIJSONStartIndex(t *testing.T) {
8585
tests := []struct {
8686
name string
8787
input string
@@ -94,7 +94,7 @@ func TestFirstJSONStartIndex(t *testing.T) {
9494

9595
for _, tt := range tests {
9696
t.Run(tt.name, func(t *testing.T) {
97-
if got := firstJSONStartIndex(tt.input); got != tt.want {
97+
if got := firstCLIJSONStartIndex(tt.input); got != tt.want {
9898
t.Fatalf("got %d want %d", got, tt.want)
9999
}
100100
})

0 commit comments

Comments
 (0)