Skip to content

Commit 08bc0a5

Browse files
bomly-guyclaude
andcommitted
fix: parse multi-module Maven TGF dependency trees
`mvn dependency:tree -DoutputType=tgf` on a multi-module reactor emits one TGF block per module (nodes, then a `#` separator, then edges), all concatenated on stdout. depGraphFromMavenTGF used a single nodes→edges flag that flipped to "edges" on the first `#` and never reset, so every module after the first had its node lines dropped and its edges then referenced ids that were never registered — failing the whole scan with "maven tgf references unknown package". Classify each line by shape instead — a node line ("<id> <coords>") or an edge line ("<from> <to> <scope>") — which handles any number of concatenated blocks. This is safe because node ids are global object hashcodes, unique across the reactor (verified against a real 13-module reactor: zero id was reused for a different coordinate across blocks), and node vs edge lines are unambiguous (a node's second field is a coordinate with colons; an edge's is a numeric id). Adds a 3-block testdata fixture + TestMavenTGFMultiModule covering nodes, edges, and scopes from the 2nd and 3rd blocks (fails on the old parser with the exact "unknown package" error). Verified end to end on the Internet2 Grouper 4.x reactor: the maven detector went from total failure to resolving 137 vulnerable packages / 191 findings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4697fc8 commit 08bc0a5

3 files changed

Lines changed: 80 additions & 16 deletions

File tree

internal/detectors/maven/detector.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ func depGraphFromMavenTGF(raw []byte) (*sdk.Graph, error) {
242242
// dependency trees (or single nodes with very long coordinate strings)
243243
// routinely exceed that and fail with "token too long", so raise the cap.
244244
scanner.Buffer(make([]byte, 0, 64*1024), maxTGFTokenSize)
245-
inEdges := false
246245

247246
tgfPackages := make(map[string]*sdk.Dependency)
248247
tgfGraph := sdk.New()
@@ -252,20 +251,31 @@ func depGraphFromMavenTGF(raw []byte) (*sdk.Graph, error) {
252251
}
253252
relationships := make([]edge, 0, 16)
254253

254+
// `mvn dependency:tree -DoutputType=tgf` on a multi-module reactor emits
255+
// ONE TGF block per module (nodes, then a `#` separator, then edges),
256+
// all concatenated on stdout. Node ids are global object hashcodes,
257+
// unique across the whole reactor — verified against a real 13-module
258+
// reactor: zero id was reused for a different coordinate across blocks.
259+
// So we classify each line by its SHAPE — a node line ("<id> <coords>")
260+
// or an edge line ("<from> <to> <scope>") — instead of relying on a
261+
// single nodes→edges transition. The previous single `#` flag flipped to
262+
// "edges" on the first module and never reset, so every later block's
263+
// node lines were dropped and their edges then referenced ids that were
264+
// never registered ("maven tgf references unknown package"). Node and
265+
// edge lines are unambiguous (a node's second field is a coordinate with
266+
// colons; an edge's second field is a numeric id), and nodes are checked
267+
// first, so a shape check is safe.
255268
for scanner.Scan() {
256269
line, ok := normalizeMavenTGFLine(scanner.Text())
257270
if !ok {
258271
continue
259272
}
260-
if line == "#" {
261-
inEdges = true
273+
switch {
274+
case line == "#":
275+
// Block/section separator; classification is by shape, so there
276+
// is nothing to track across it.
262277
continue
263-
}
264-
265-
if !inEdges {
266-
if !looksLikeTGFNodeLine(line) {
267-
continue
268-
}
278+
case looksLikeTGFNodeLine(line):
269279
id, node, err := parseTGFNodeLine(line)
270280
if err != nil {
271281
return nil, err
@@ -276,14 +286,10 @@ func depGraphFromMavenTGF(raw []byte) (*sdk.Graph, error) {
276286
} else if err := tgfGraph.AddNode(node); err != nil && !errors.Is(err, sdk.ErrNodeAlreadyExist) {
277287
return nil, fmt.Errorf("add maven package %q: %w", node.ID, err)
278288
}
279-
continue
280-
}
281-
282-
if !looksLikeTGFEdgeLine(line) {
283-
continue
289+
case looksLikeTGFEdgeLine(line):
290+
fields := strings.Fields(line)
291+
relationships = append(relationships, edge{from: fields[0], to: fields[1]})
284292
}
285-
fields := strings.Fields(line)
286-
relationships = append(relationships, edge{from: fields[0], to: fields[1]})
287293
}
288294

289295
if err := scanner.Err(); err != nil {

internal/detectors/maven/fixture_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,48 @@ import (
88
"github.com/bomly-dev/bomly-cli/sdk"
99
)
1010

11+
// TestMavenTGFMultiModule verifies the parser handles a multi-module reactor,
12+
// where `mvn dependency:tree -DoutputType=tgf` emits one TGF block per module
13+
// (nodes, `#`, edges) concatenated. Regression for a real failure on a
14+
// 13-module reactor: the old single nodes→edges flag dropped every block after
15+
// the first, so their edges referenced ids that were never registered
16+
// ("maven tgf references unknown package"). The fixture has three blocks; the
17+
// assertions cover nodes and edges from the 2nd and 3rd blocks specifically.
18+
func TestMavenTGFMultiModule(t *testing.T) {
19+
raw, err := os.ReadFile(filepath.Join("testdata", "dependency-tree-multimodule.tgf"))
20+
if err != nil {
21+
t.Fatalf("read fixture: %v", err)
22+
}
23+
g, err := depGraphFromMavenTGF(raw)
24+
if err != nil {
25+
t.Fatalf("depGraphFromMavenTGF: %v", err)
26+
}
27+
28+
for _, want := range []string{
29+
"com.bomly:module-a@1.0.0",
30+
"org.apache.commons:commons-lang3@3.12.0",
31+
"com.bomly:module-b@1.0.0",
32+
"com.fasterxml.jackson.core:jackson-databind@2.13.0",
33+
"org.yaml:snakeyaml@1.30",
34+
"com.bomly:module-c@1.0.0",
35+
"junit:junit@4.13.2",
36+
"org.hamcrest:hamcrest-core@1.3",
37+
} {
38+
if _, ok := g.Node(want); !ok {
39+
t.Errorf("missing node %s", want)
40+
}
41+
}
42+
43+
// Edges from the 2nd and 3rd blocks — the ones the old parser lost.
44+
requireMavenEdge(t, g, "com.bomly:module-b@1.0.0", "com.fasterxml.jackson.core:jackson-databind@2.13.0")
45+
requireMavenEdge(t, g, "com.fasterxml.jackson.core:jackson-databind@2.13.0", "org.yaml:snakeyaml@1.30")
46+
requireMavenEdge(t, g, "com.bomly:module-c@1.0.0", "junit:junit@4.13.2")
47+
requireMavenEdge(t, g, "junit:junit@4.13.2", "org.hamcrest:hamcrest-core@1.3")
48+
49+
requireMavenScope(t, g, "org.yaml:snakeyaml@1.30", sdk.ScopeRuntime)
50+
requireMavenScope(t, g, "junit:junit@4.13.2", sdk.ScopeDevelopment)
51+
}
52+
1153
// TestMavenTGFFixture drives the dependency-tree (TGF) parser against a committed
1254
// fixture captured from `mvn dependency:tree -DoutputType=tgf`, so it exercises
1355
// the real output shape without invoking Maven.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
105194717 com.bomly:module-a:jar:1.0.0
2+
366752671 org.apache.commons:commons-lang3:jar:3.12.0:compile
3+
#
4+
105194717 366752671 compile
5+
773708944 com.bomly:module-b:jar:1.0.0
6+
834153999 com.fasterxml.jackson.core:jackson-databind:jar:2.13.0:compile
7+
399683701 org.yaml:snakeyaml:jar:1.30:compile
8+
#
9+
773708944 834153999 compile
10+
834153999 399683701 compile
11+
1274547241 com.bomly:module-c:jar:1.0.0
12+
1364958538 junit:junit:jar:4.13.2:test
13+
1858779250 org.hamcrest:hamcrest-core:jar:1.3:test
14+
#
15+
1274547241 1364958538 test
16+
1364958538 1858779250 test

0 commit comments

Comments
 (0)