Skip to content

Commit d5aa39b

Browse files
committed
fix(db_import_vigolium): stream JSONL with bufio.Reader to handle oversized lines
Vigolium http_record and finding entries can embed multi-MB response bodies, producing JSONL lines that exceed bufio.Scanner's max token size and fail with 'token too long'. Switch to bufio.Reader.ReadString, which grows as needed, and extract per-line logic into a processLine closure. Add a regression test that imports a finding with a 12MB response body.
1 parent 8965274 commit d5aa39b

2 files changed

Lines changed: 76 additions & 20 deletions

File tree

internal/functions/db_functions.go

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"fmt"
77
"github.com/j3ssie/osmedeus/v5/internal/json"
8+
"io"
89
"net"
910
"os"
1011
"path/filepath"
@@ -3275,17 +3276,11 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
32753276
skipped := 0
32763277
total := 0
32773278

3278-
scanner := bufio.NewScanner(file)
3279-
buf := make([]byte, 0, 10*1024*1024) // 10MB buffer
3280-
scanner.Buffer(buf, 10*1024*1024)
3281-
32823279
now := time.Now()
32833280

3284-
for scanner.Scan() {
3285-
line := strings.TrimSpace(scanner.Text())
3286-
if line == "" {
3287-
continue
3288-
}
3281+
// processLine handles a single JSONL record. Extracted into a closure so the
3282+
// streaming reader below can stay a tight loop; early-outs use return.
3283+
processLine := func(line string) {
32893284
total++
32903285

32913286
// Peek at the envelope: {"type":"...","data":{...}}
@@ -3295,7 +3290,7 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
32953290
}
32963291
if err := json.Unmarshal([]byte(line), &envelope); err != nil || envelope.Type == "" || len(envelope.Data) == 0 {
32973292
skipped++
3298-
continue
3293+
return
32993294
}
33003295

33013296
switch envelope.Type {
@@ -3304,14 +3299,14 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
33043299
extracted, skip := unwrapEnvelopeJSON([]byte(line))
33053300
if skip || extracted == nil {
33063301
assetStats.Errors++
3307-
continue
3302+
return
33083303
}
33093304

33103305
var asset database.Asset
33113306
if err := unmarshalAssetJSON(extracted, &asset); err != nil {
33123307
logger.Get().Debug("vigolium: skipping invalid http_record", zap.Error(err))
33133308
assetStats.Errors++
3314-
continue
3309+
return
33153310
}
33163311

33173312
asset.Workspace = workspace
@@ -3337,7 +3332,7 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
33373332
if _, insertErr := db.NewInsert().Model(&asset).Exec(ctx); insertErr != nil {
33383333
logger.Get().Debug("vigolium: failed to insert asset", zap.Error(insertErr))
33393334
assetStats.Errors++
3340-
continue
3335+
return
33413336
}
33423337
assetStats.New++
33433338
} else {
@@ -3346,7 +3341,7 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
33463341
if _, updateErr := db.NewUpdate().Model(&asset).WherePK().Exec(ctx); updateErr != nil {
33473342
logger.Get().Debug("vigolium: failed to update asset", zap.Error(updateErr))
33483343
assetStats.Errors++
3349-
continue
3344+
return
33503345
}
33513346
assetStats.Updated++
33523347
}
@@ -3356,7 +3351,7 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
33563351
if err := json.Unmarshal(envelope.Data, &fields); err != nil {
33573352
logger.Get().Debug("vigolium: skipping invalid finding", zap.Error(err))
33583353
vulnStats.Errors++
3359-
continue
3354+
return
33603355
}
33613356

33623357
vuln := mapVigoliumFindingToVuln(fields, workspace, line)
@@ -3378,7 +3373,7 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
33783373
if _, insertErr := db.NewInsert().Model(&vuln).Exec(ctx); insertErr != nil {
33793374
logger.Get().Debug("vigolium: failed to insert vuln", zap.Error(insertErr))
33803375
vulnStats.Errors++
3381-
continue
3376+
return
33823377
}
33833378
vulnStats.New++
33843379
} else {
@@ -3388,7 +3383,7 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
33883383
if _, updateErr := db.NewUpdate().Model(&vuln).WherePK().Exec(ctx); updateErr != nil {
33893384
logger.Get().Debug("vigolium: failed to update vuln", zap.Error(updateErr))
33903385
vulnStats.Errors++
3391-
continue
3386+
return
33923387
}
33933388
vulnStats.Updated++
33943389
} else {
@@ -3397,7 +3392,7 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
33973392
Where("id = ?", existing.ID).
33983393
Exec(ctx); updateErr != nil {
33993394
vulnStats.Errors++
3400-
continue
3395+
return
34013396
}
34023397
vulnStats.Unchanged++
34033398
}
@@ -3409,8 +3404,21 @@ func (vf *vmFunc) dbImportVigolium(call goja.FunctionCall) goja.Value {
34093404
}
34103405
}
34113406

3412-
if err := scanner.Err(); err != nil {
3413-
return vf.errorValue(fmt.Sprintf("error reading file: %v", err))
3407+
// Stream the file with a bufio.Reader rather than bufio.Scanner: vigolium
3408+
// http_record lines can embed multi-MB response bodies, which overflow the
3409+
// Scanner's max-token limit ("token too long"). ReadString grows as needed.
3410+
reader := bufio.NewReaderSize(file, 1024*1024) // 1MB read buffer
3411+
for {
3412+
lineStr, readErr := reader.ReadString('\n')
3413+
if line := strings.TrimSpace(lineStr); line != "" {
3414+
processLine(line)
3415+
}
3416+
if readErr != nil {
3417+
if readErr != io.EOF {
3418+
return vf.errorValue(fmt.Sprintf("error reading file: %v", readErr))
3419+
}
3420+
break
3421+
}
34143422
}
34153423

34163424
logger.Get().Info("db_import_vigolium: import completed",

internal/functions/db_vigolium_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package functions
33
import (
44
"context"
55
"os"
6+
"path/filepath"
7+
"strings"
68
"testing"
79

810
"github.com/j3ssie/osmedeus/v5/internal/database"
@@ -119,6 +121,52 @@ func TestDbImportVigolium_Idempotent(t *testing.T) {
119121
assert.Equal(t, 146, vulnCount, "no duplicate vulns after re-import")
120122
}
121123

124+
// A single finding record can embed a multi-MB response body, producing a JSONL
125+
// line longer than bufio.Scanner's max token size. The reader-based import must
126+
// stream these without "token too long".
127+
func TestDbImportVigolium_OversizedLine(t *testing.T) {
128+
cleanup := setupTestDB(t)
129+
defer cleanup()
130+
131+
registry := NewRegistry()
132+
133+
// 12MB response body — comfortably past the old 10MB scanner buffer.
134+
bigBody := strings.Repeat("A", 12*1024*1024)
135+
line := `{"type":"finding","data":{` +
136+
`"finding_hash":"oversized-line-hash",` +
137+
`"module_id":"big-response",` +
138+
`"module_name":"Big Response",` +
139+
`"severity":"high",` +
140+
`"url":"https://example.com/big",` +
141+
`"response":"` + bigBody + `"}}`
142+
143+
testFile := filepath.Join(t.TempDir(), "vigolium-oversized.jsonl")
144+
require.NoError(t, os.WriteFile(testFile, []byte(line+"\n"), 0o644))
145+
146+
result, err := registry.Execute(
147+
`db_import_vigolium("test-workspace", "`+testFile+`")`,
148+
map[string]interface{}{},
149+
)
150+
require.NoError(t, err)
151+
152+
stats, ok := result.(map[string]interface{})
153+
require.True(t, ok, "result should be a nested stats map, got: %v", result)
154+
155+
vulnStats := stats["vulns"].(map[string]interface{})
156+
assert.Equal(t, 1, vulnStats["new"], "oversized finding should import")
157+
assert.Equal(t, 0, vulnStats["errors"])
158+
assert.Equal(t, 1, stats["total"])
159+
160+
ctx := context.Background()
161+
db := database.GetDB()
162+
var vuln database.Vulnerability
163+
err = db.NewSelect().Model(&vuln).
164+
Where("finding_hash = ?", "oversized-line-hash").Scan(ctx)
165+
require.NoError(t, err)
166+
assert.Equal(t, "big-response", vuln.VulnInfo)
167+
assert.Len(t, vuln.DetailHTTPResponse, 12*1024*1024)
168+
}
169+
122170
func TestDbImportVigolium_EmptyArgs(t *testing.T) {
123171
cleanup := setupTestDB(t)
124172
defer cleanup()

0 commit comments

Comments
 (0)