Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/deps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ These manifests pin **exact versions** (or lockfile-resolved versions) of packag
| `python/pyproject-app/poetry.lock` | resolved | `requests` | `2.31.0` | (from lock; resolves `>=2.31.0` in pyproject.toml) | — |
| `python/uv-app/pyproject.toml` / `uv.lock` | `[project.dependencies]` | `urllib3` | `1.26.4` | `CVE-2021-33503` | `>= 1.26.5` |
| `python/uv-app/uv.lock` | resolved | `requests` | `2.31.0` | (from lock; resolves `>=2.31.0` in pyproject.toml) | — |
| `go/mod-app/go.mod` | `require` (direct) | `golang.org/x/crypto` | `0.16.0` | `CVE-2023-48795` | `>= 0.17.0` |
| `go/mod-app/go.mod` | `require` (direct) | `github.com/go-jose/go-jose/v3` | `3.0.0` | `CVE-2024-28176` | `>= 3.0.3` |
| `go/mod-app/go.mod` | `require` (indirect) | `github.com/google/uuid` | `1.6.0` | (no known advisory) | — |

Run against the full fixture tree:

Expand Down
12 changes: 12 additions & 0 deletions examples/deps/go/mod-app/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module example.com/vulnerable-go-app

go 1.21

require (
golang.org/x/crypto v0.16.0
github.com/go-jose/go-jose/v3 v3.0.0
)

require (
github.com/google/uuid v1.6.0 // indirect
)
4 changes: 4 additions & 0 deletions src/scan/deps/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { extractPnpmLockDependencies } from "./extract/pnpmLock";
import { extractPyprojectTomlDependencies } from "./extract/pyprojectToml";
import { extractRequirementsTxtDependencies } from "./extract/requirementsTxt";
import { extractUvLockDependencies } from "./extract/uvLock";
import { extractGoModDependencies } from "./extract/goMod";
import {
DependencyExtractionResult,
NPM_ECOSYSTEM,
Expand Down Expand Up @@ -101,6 +102,9 @@ export function extractDependenciesForManifestWithDiagnostics(
if (baseName === "pyproject.toml") {
return extractPyprojectTomlDependencies(manifestPath);
}
if (baseName === "go.mod") {
return extractGoModDependencies(manifestPath);
}
return { dependencies: [], warnings: [] };
}

Expand Down
119 changes: 119 additions & 0 deletions src/scan/deps/extract/goMod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { DependencyCoordinate } from "../types";
import {
DependencyExtractionResult,
dedupeCoordinates,
emptyExtractionResult,
manifestReadWarning,
readManifestSource
} from "./shared";

export const GO_ECOSYSTEM = "Go";

// Matches a module path + semver version tag on a require line.
// Module path: printable, no whitespace; version: v<major>.<minor>.<patch>[prerelease/build]
const GO_REQUIRE_LINE_RE =
/^([A-Za-z0-9][A-Za-z0-9.\-_/]*(?:\/v\d+)?)\s+(v\d+\.\d+\.\d+(?:[.\-][0-9A-Za-z.-]*)*)/;

// Pseudo-version pattern — skip these as they carry no meaningful semver for OSV.
const GO_PSEUDO_VERSION_RE = /^v\d+\.\d+\.\d+-\d{14}-[0-9a-f]{12}$/;

function parseGoVersion(rawVersion: string): string | null {
if (GO_PSEUDO_VERSION_RE.test(rawVersion)) {
return null;
}
// Strip the mandatory 'v' prefix; OSV Go ecosystem uses bare semver.
const match = rawVersion.match(/^v(\d+\.\d+\.\d+(?:[.\-][0-9A-Za-z.-]*)?)$/);
return match?.[1] ?? null;
}

function parseRequireLine(
line: string
): { name: string; version: string } | null {
// Strip inline comment (e.g. "// indirect", "// indirect; go 1.17")
const withoutComment = line.split("//")[0]?.trim() ?? "";
const match = withoutComment.match(GO_REQUIRE_LINE_RE);
if (!match) {
return null;
}

const name = match[1];
const rawVersion = match[2];
if (!name || !rawVersion) {
return null;
}

const version = parseGoVersion(rawVersion);
if (!version) {
return null;
}

return { name, version };
}

export function extractGoModDependencies(manifestPath: string): DependencyExtractionResult {
const readResult = readManifestSource(manifestPath);
if (!readResult.source) {
if (readResult.warning) {
return emptyExtractionResult(manifestReadWarning(readResult.absolutePath, readResult.warning));
}
return emptyExtractionResult();
}

const source = readResult.source;
const lines = source.split(/\r?\n/);
const dependencies: DependencyCoordinate[] = [];
let inRequireBlock = false;

for (let index = 0; index < lines.length; index++) {
const raw = lines[index] ?? "";
const line = raw.trim();

if (!inRequireBlock) {
// Opening block: require ( ...
if (/^require\s*\($/.test(line)) {
inRequireBlock = true;
continue;
}

// Single-line: require module/path v1.2.3
if (line.startsWith("require ")) {
const rest = line.slice("require ".length).trim();
const parsed = parseRequireLine(rest);
if (parsed) {
dependencies.push({
ecosystem: GO_ECOSYSTEM,
name: parsed.name,
version: parsed.version,
manifestPath: readResult.absolutePath,
manifestLine: index + 1
});
}
}
} else {
if (line === ")") {
inRequireBlock = false;
continue;
}

if (!line || line.startsWith("//")) {
continue;
}

const parsed = parseRequireLine(line);
if (parsed) {
dependencies.push({
ecosystem: GO_ECOSYSTEM,
name: parsed.name,
version: parsed.version,
manifestPath: readResult.absolutePath,
manifestLine: index + 1
});
}
}
}

return {
dependencies: dedupeCoordinates(dependencies),
warnings: []
};
}
15 changes: 15 additions & 0 deletions tests/depsExamples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ScanContext } from "../src/scan/types";

const FIXTURE_ROOT = path.join(process.cwd(), "examples", "deps", "npm");
const PYTHON_FIXTURE_ROOT = path.join(process.cwd(), "examples", "deps", "python");
const GO_FIXTURE_ROOT = path.join(process.cwd(), "examples", "deps", "go");
const OSV_RUNTIME_APP_BATCH = path.join(
process.cwd(),
"tests",
Expand Down Expand Up @@ -177,3 +178,17 @@ test("examples deps fixtures normalize stubbed OSV batch into findings with CVE
globalThis.fetch = originalFetch;
}
});

test("examples go deps fixtures expose exact Go coordinates", () => {
const manifests = [path.join(GO_FIXTURE_ROOT, "mod-app", "go.mod")];

const coordinates = manifests.flatMap((manifestPath) => extractDependenciesForManifest(manifestPath));
const labels = coordinates.map((dep) => `${dep.ecosystem}:${dep.name}@${dep.version}`).sort();

assert.deepEqual(labels, [
"Go:github.com/go-jose/go-jose/v3@3.0.0",
"Go:github.com/google/uuid@1.6.0",
"Go:golang.org/x/crypto@0.16.0"
]);
assert.ok(coordinates.every((dep) => dep.manifestLine > 0));
});
133 changes: 133 additions & 0 deletions tests/depsExtraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,3 +649,136 @@ test("collectDependencies does not warn about unscoped lockfiles when lockfile i

fs.rmSync(tmpDir, { recursive: true, force: true });
});

test("extractDependenciesForManifest reads go.mod block require entries", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codefence-gomod-block-"));
const manifestPath = path.join(tmpDir, "go.mod");
fs.writeFileSync(
manifestPath,
[
"module example.com/myapp",
"",
"go 1.21",
"",
"require (",
"\tgolang.org/x/crypto v0.16.0",
"\tgithub.com/gin-gonic/gin v1.8.1",
"\tgithub.com/google/uuid v1.6.0 // indirect",
")",
""
].join("\n"),
"utf8"
);

const result = extractDependenciesForManifestWithDiagnostics(manifestPath);
assert.deepEqual(
result.dependencies.map((dep) => `${dep.ecosystem}:${dep.name}@${dep.version}`).sort(),
[
"Go:github.com/gin-gonic/gin@1.8.1",
"Go:github.com/google/uuid@1.6.0",
"Go:golang.org/x/crypto@0.16.0"
]
);
assert.equal(result.dependencies.find((dep) => dep.name === "golang.org/x/crypto")?.manifestLine, 6);
assert.equal(result.warnings.length, 0);

fs.rmSync(tmpDir, { recursive: true, force: true });
});

test("extractDependenciesForManifest reads go.mod single-line require entries", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codefence-gomod-single-"));
const manifestPath = path.join(tmpDir, "go.mod");
fs.writeFileSync(
manifestPath,
[
"module example.com/myapp",
"",
"go 1.21",
"",
"require golang.org/x/crypto v0.16.0",
"require github.com/go-jose/go-jose/v3 v3.0.0",
""
].join("\n"),
"utf8"
);

const result = extractDependenciesForManifestWithDiagnostics(manifestPath);
assert.deepEqual(
result.dependencies.map((dep) => `${dep.ecosystem}:${dep.name}@${dep.version}`).sort(),
[
"Go:github.com/go-jose/go-jose/v3@3.0.0",
"Go:golang.org/x/crypto@0.16.0"
]
);
assert.equal(result.dependencies.find((dep) => dep.name === "golang.org/x/crypto")?.manifestLine, 5);
assert.equal(result.warnings.length, 0);

fs.rmSync(tmpDir, { recursive: true, force: true });
});

test("extractDependenciesForManifest skips pseudo-versions in go.mod", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codefence-gomod-pseudo-"));
const manifestPath = path.join(tmpDir, "go.mod");
fs.writeFileSync(
manifestPath,
[
"module example.com/myapp",
"",
"go 1.21",
"",
"require (",
"\tgolang.org/x/crypto v0.16.0",
"\tgithub.com/some/dev-dep v0.0.0-20231113122135-a4f7c8f4c9d3",
")",
""
].join("\n"),
"utf8"
);

const result = extractDependenciesForManifestWithDiagnostics(manifestPath);
assert.deepEqual(
result.dependencies.map((dep) => `${dep.ecosystem}:${dep.name}@${dep.version}`),
["Go:golang.org/x/crypto@0.16.0"]
);
assert.equal(result.warnings.length, 0);

fs.rmSync(tmpDir, { recursive: true, force: true });
});

test("extractDependenciesForManifest returns empty result for malformed go.mod", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codefence-gomod-malformed-"));
const manifestPath = path.join(tmpDir, "go.mod");
fs.writeFileSync(manifestPath, "not a valid go.mod\n!!!###", "utf8");

const result = extractDependenciesForManifestWithDiagnostics(manifestPath);
assert.deepEqual(result.dependencies, []);
assert.equal(result.warnings.length, 0);

fs.rmSync(tmpDir, { recursive: true, force: true });
});

test("extractDependenciesForManifest deduplicates go.mod entries", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "codefence-gomod-dedup-"));
const manifestPath = path.join(tmpDir, "go.mod");
fs.writeFileSync(
manifestPath,
[
"module example.com/myapp",
"",
"go 1.21",
"",
"require (",
"\tgolang.org/x/crypto v0.16.0",
"\tgolang.org/x/crypto v0.16.0",
")",
""
].join("\n"),
"utf8"
);

const result = extractDependenciesForManifestWithDiagnostics(manifestPath);
assert.equal(result.dependencies.length, 1);
assert.equal(result.dependencies[0]?.name, "golang.org/x/crypto");

fs.rmSync(tmpDir, { recursive: true, force: true });
});