Skip to content

Commit b30aa35

Browse files
authored
feat: add Go stdlib vulnerability detection to container scans (#767)
Extract the Go compiler version from binary buildinfo and add a `stdlib` pseudo-dependency node to the dependency graph. This enables vuln-service to match Go standard library vulnerabilities for container images, closing the gap with Snyk Open Source support. Works for both normal and stripped binaries since .go.buildinfo is always present regardless of build flags.
1 parent 2e76901 commit b30aa35

File tree

6 files changed

+257
-19
lines changed

6 files changed

+257
-19
lines changed

lib/go-parser/go-binary.ts

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ const debug = Debug("snyk");
4444
export class GoBinary {
4545
public name: string;
4646
public modules: GoModule[];
47+
public goVersion: string;
4748
private hasPclnTab: boolean;
4849

4950
constructor(goElfBinary: Elf) {
50-
[this.name, this.modules] = extractModuleInformation(goElfBinary);
51+
[this.name, this.modules, this.goVersion] =
52+
extractModuleInformation(goElfBinary);
5153

5254
const pclnTab = goElfBinary.body.sections.find(
5355
(section) => section.name === ".gopclntab",
@@ -108,6 +110,19 @@ export class GoBinary {
108110
// else: pclnTab exists but module has no packages - don't report anything
109111
}
110112

113+
if (this.goVersion) {
114+
const stdlibNodeId = `stdlib@${this.goVersion}`;
115+
goModulesDepGraph.addPkgNode(
116+
{ name: "stdlib", version: this.goVersion },
117+
stdlibNodeId,
118+
);
119+
goModulesDepGraph.connectDep(goModulesDepGraph.rootNodeId, stdlibNodeId);
120+
} else {
121+
debug(
122+
`Skipping stdlib node for ${this.name}: could not parse Go version`,
123+
);
124+
}
125+
111126
return goModulesDepGraph.build();
112127
}
113128

@@ -184,15 +199,36 @@ interface GoFileNameError extends Error {
184199
moduleName: string;
185200
}
186201

202+
/**
203+
* Strips the "go" prefix from a Go version string and validates the format.
204+
* Returns the cleaned version (e.g., "1.21.0") or empty string if invalid.
205+
* Rejects RC/beta/devel versions since we cannot accurately match vulnerabilities
206+
* against pre-release builds.
207+
*/
208+
export function parseGoVersion(rawVersion: string): string {
209+
// Only match release versions (e.g., "go1.21" or "go1.21.5").
210+
// Reject RC/beta (go1.21rc1, go1.22beta2) and devel builds.
211+
const match = rawVersion.match(/^go(\d+\.\d+(?:\.\d+)?)$/);
212+
if (!match) {
213+
return "";
214+
}
215+
const ver = match[1];
216+
// Ensure three-segment semver (e.g., "1.19" → "1.19.0") because
217+
// @snyk/vuln uses node's semver library which requires three segments.
218+
return ver.includes(".", ver.indexOf(".") + 1) ? ver : ver + ".0";
219+
}
220+
187221
export function extractModuleInformation(
188222
binary: Elf,
189-
): [name: string, deps: GoModule[]] {
190-
const mod = readRawBuildInfo(binary);
191-
if (!mod) {
223+
): [name: string, deps: GoModule[], goVersion: string] {
224+
const { goVersion: rawGoVersion, modInfo } = readRawBuildInfo(binary);
225+
if (!modInfo) {
192226
throw Error("binary contains empty module info");
193227
}
194228

195-
const [pathDirective, mainModuleLine, ...versionsLines] = mod
229+
const goVersion = parseGoVersion(rawGoVersion);
230+
231+
const [pathDirective, mainModuleLine, ...versionsLines] = modInfo
196232
.replace("\r", "")
197233
.split("\n");
198234
const lineSplit = mainModuleLine.split("\t");
@@ -224,7 +260,7 @@ export function extractModuleInformation(
224260
}
225261
});
226262

227-
return [name, modules];
263+
return [name, modules, goVersion];
228264
}
229265

230266
// Source
@@ -234,7 +270,12 @@ export function extractModuleInformation(
234270
* module version information in the executable binary
235271
* @param binary
236272
*/
237-
export function readRawBuildInfo(binary: Elf): string {
273+
export interface RawBuildInfo {
274+
goVersion: string;
275+
modInfo: string;
276+
}
277+
278+
export function readRawBuildInfo(binary: Elf): RawBuildInfo {
238279
const buildInfoMagic = "\xff Go buildinf:";
239280
// Read the first 64kB of dataAddr to find the build info blob.
240281
// On some platforms, the blob will be in its own section, and DataStart
@@ -272,9 +313,9 @@ export function readRawBuildInfo(binary: Elf): string {
272313
const ptrSize = data[14];
273314
if ((data[15] & 2) !== 0) {
274315
data = data.subarray(32);
275-
[, data] = decodeString(data);
276-
const [mod] = decodeString(data);
277-
return mod;
316+
const [goVersion, rest] = decodeString(data);
317+
const [mod] = decodeString(rest);
318+
return { goVersion, modInfo: mod };
278319
} else {
279320
const bigEndian = data[15] !== 0;
280321

@@ -326,7 +367,7 @@ export function readRawBuildInfo(binary: Elf): string {
326367
// First 16 bytes are unicodes as last 16
327368
// Mirrors go version source code
328369
if (mod.length >= 33 && mod[mod.length - 17] === "\n") {
329-
return mod.slice(16, mod.length - 16);
370+
return { goVersion: version, modInfo: mod.slice(16, mod.length - 16) };
330371
} else {
331372
throw Error("binary is not built with go module support");
332373
}

test/system/application-scans/__snapshots__/gomodules.spec.ts.snap

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,9 @@ Object {
990990
Object {
991991
"nodeId": "gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776",
992992
},
993+
Object {
994+
"nodeId": "stdlib@1.15.1",
995+
},
993996
],
994997
"nodeId": "root-node",
995998
"pkgId": "github.com/mikefarah/yq/v3@",
@@ -1074,6 +1077,11 @@ Object {
10741077
"nodeId": "gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776",
10751078
"pkgId": "gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776",
10761079
},
1080+
Object {
1081+
"deps": Array [],
1082+
"nodeId": "stdlib@1.15.1",
1083+
"pkgId": "stdlib@1.15.1",
1084+
},
10771085
],
10781086
"rootNodeId": "root-node",
10791087
},
@@ -1199,6 +1207,13 @@ Object {
11991207
"version": "v3.0.0-20200615113413-eeeca48fe776",
12001208
},
12011209
},
1210+
Object {
1211+
"id": "stdlib@1.15.1",
1212+
"info": Object {
1213+
"name": "stdlib",
1214+
"version": "1.15.1",
1215+
},
1216+
},
12021217
],
12031218
"schemaVersion": "1.3.0",
12041219
},
@@ -1369,6 +1384,9 @@ Object {
13691384
Object {
13701385
"nodeId": "gopkg.in/yaml.v2@v2.4.0",
13711386
},
1387+
Object {
1388+
"nodeId": "stdlib@1.17.11",
1389+
},
13721390
],
13731391
"nodeId": "root-node",
13741392
"pkgId": "testgo@",
@@ -1423,6 +1441,11 @@ Object {
14231441
"nodeId": "gopkg.in/yaml.v2@v2.4.0",
14241442
"pkgId": "gopkg.in/yaml.v2@v2.4.0",
14251443
},
1444+
Object {
1445+
"deps": Array [],
1446+
"nodeId": "stdlib@1.17.11",
1447+
"pkgId": "stdlib@1.17.11",
1448+
},
14261449
],
14271450
"rootNodeId": "root-node",
14281451
},
@@ -1506,6 +1529,13 @@ Object {
15061529
"version": "v2.4.0",
15071530
},
15081531
},
1532+
Object {
1533+
"id": "stdlib@1.17.11",
1534+
"info": Object {
1535+
"name": "stdlib",
1536+
"version": "1.17.11",
1537+
},
1538+
},
15091539
],
15101540
"schemaVersion": "1.3.0",
15111541
},
@@ -1676,6 +1706,9 @@ Object {
16761706
Object {
16771707
"nodeId": "gopkg.in/yaml.v2@v2.4.0",
16781708
},
1709+
Object {
1710+
"nodeId": "stdlib@1.18.3",
1711+
},
16791712
],
16801713
"nodeId": "root-node",
16811714
"pkgId": "testgo@",
@@ -1730,6 +1763,11 @@ Object {
17301763
"nodeId": "gopkg.in/yaml.v2@v2.4.0",
17311764
"pkgId": "gopkg.in/yaml.v2@v2.4.0",
17321765
},
1766+
Object {
1767+
"deps": Array [],
1768+
"nodeId": "stdlib@1.18.3",
1769+
"pkgId": "stdlib@1.18.3",
1770+
},
17331771
],
17341772
"rootNodeId": "root-node",
17351773
},
@@ -1813,6 +1851,13 @@ Object {
18131851
"version": "v2.4.0",
18141852
},
18151853
},
1854+
Object {
1855+
"id": "stdlib@1.18.3",
1856+
"info": Object {
1857+
"name": "stdlib",
1858+
"version": "1.18.3",
1859+
},
1860+
},
18161861
],
18171862
"schemaVersion": "1.3.0",
18181863
},

test/system/application-scans/gomodules.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ describe("Stripped Go binary without .gopclntab: no-pcln-tab fixture", () => {
155155
expect(found).toBeDefined();
156156
});
157157

158-
expect(deps.length).toBe(expectedDepsNoPcln.length);
158+
// +1 for stdlib pseudo-dependency when goVersion is present
159+
expect(deps.length).toBe(expectedDepsNoPcln.length + 1);
159160
expect(depGraph.rootPkg.name).toBe(
160161
"github.com/rootless-containers/rootlesskit",
161162
);
@@ -231,7 +232,8 @@ describe("Stripped and CGo Go binaries detection scan handler test", () => {
231232
});
232233

233234
if (detectedBinaries.fleetServer) {
234-
expect(detectedBinaries.fleetServer.moduleCount).toEqual(76);
235+
// +1 for stdlib pseudo-dependency when goVersion is present
236+
expect(detectedBinaries.fleetServer.moduleCount).toEqual(77);
235237
} else {
236238
fail("fleet-server not detected");
237239
}

test/system/operating-systems/__snapshots__/sles15.spec.ts.snap

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2225,6 +2225,9 @@ Object {
22252225
Object {
22262226
"nodeId": "github.com/xrash/smetrics@v0.0.0-20201216005158-039620a65673",
22272227
},
2228+
Object {
2229+
"nodeId": "stdlib@1.21.5",
2230+
},
22282231
],
22292232
"nodeId": "root-node",
22302233
"pkgId": "github.com/SUSE/container-suseconnect@",
@@ -2249,6 +2252,11 @@ Object {
22492252
"nodeId": "github.com/xrash/smetrics@v0.0.0-20201216005158-039620a65673",
22502253
"pkgId": "github.com/xrash/smetrics@v0.0.0-20201216005158-039620a65673",
22512254
},
2255+
Object {
2256+
"deps": Array [],
2257+
"nodeId": "stdlib@1.21.5",
2258+
"pkgId": "stdlib@1.21.5",
2259+
},
22522260
],
22532261
"rootNodeId": "root-node",
22542262
},
@@ -2290,6 +2298,13 @@ Object {
22902298
"version": "v0.0.0-20201216005158-039620a65673",
22912299
},
22922300
},
2301+
Object {
2302+
"id": "stdlib@1.21.5",
2303+
"info": Object {
2304+
"name": "stdlib",
2305+
"version": "1.21.5",
2306+
},
2307+
},
22932308
],
22942309
"schemaVersion": "1.3.0",
22952310
},

0 commit comments

Comments
 (0)