From 49d82b94f773a47dc193c2e1da4318b56356a4ef Mon Sep 17 00:00:00 2001 From: Kunal Jaura Date: Mon, 14 Sep 2026 07:46:16 -0700 Subject: [PATCH] feat(go): OSV dependency scanning for go.mod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OSV.dev core already covers the Go ecosystem; this adds the missing go.mod extractor and indexes the file so it reaches the scanner. - _extract_go parses direct and indirect `require`s (block and single-line forms), strips the leading `v` to OSV's semver (preserving +incompatible and pseudo-versions), and skips module/go/toolchain/replace/exclude/retract. - go.mod added to KEY_FILE_NAMES so ingestion indexes it — without this the OSV scanner (which reads repo.file_index) never saw it and returned clean on a vulnerable project. (Found by live testing, not unit tests.) Verified live end-to-end (real `isitsecure` scan of a go.mod project): - github.com/dgrijalva/jwt-go v3.2.0+incompatible -> 2 OSV vulnerabilities, grade dropped to C+. - Extractor unit test hit the OSV API and returned findings for jwt-go, golang.org/x/crypto, and gin. - Full suite: 2612 passed, 1 xfailed. Go is now a fully first-class language: injection SAST + route mapping + gopls-refined per-route auth + go.mod dependency (SCA) scanning. README marks Go Dependency Scan as Yes (go.mod). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EZ4QoTqRoYWE25CNoV2Wfy --- README.md | 4 +- .../code_analysis/osv_dependency_scanner.py | 47 ++++++++++++++++++- isitsecure/engine/constants.py | 2 + tests/engine/test_osv_dependency_scanner.py | 36 +++++++++++++- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2e5a81d..dcad278 100644 --- a/README.md +++ b/README.md @@ -311,10 +311,10 @@ record to prove it could is not worth the finding. | **TypeScript/JavaScript** (Next.js, Express, tRPC, GraphQL) | Yes | Yes | Yes (npm) | Yes | | **Python** (Django, FastAPI, Flask) | Yes | Yes | Yes (pip) | Yes | | **Java/Kotlin** (Spring Boot) | Yes | Yes | Yes (Maven, Gradle) | Yes | -| **Go** (net/http, Gin, Echo, chi, gorilla) | Yes | Yes | No | Yes | +| **Go** (net/http, Gin, Echo, chi, gorilla) | Yes | Yes | Yes (go.mod) | Yes | | **Ruby, Rust, PHP, etc.** | No | No | No | Yes (DAST works against any HTTP API) | -DAST scanners test live HTTP endpoints regardless of backend language. SAST route mapping, auth detection, and dependency scanning are language-specific. Go auth detection is per-route (router/group middleware, in-handler checks) and the gopls LSP refines it — following a cross-file auth helper via go-to-definition to confirm a guard or suppress a false "missing auth". Dependency (go.mod) scanning is the one Go gap. +DAST scanners test live HTTP endpoints regardless of backend language. SAST route mapping, auth detection, and dependency scanning are language-specific. Go auth detection is per-route (router/group middleware, in-handler checks) and the gopls LSP refines it — following a cross-file auth helper via go-to-definition to confirm a guard or suppress a false "missing auth". Go module dependencies (`go.mod`, direct and indirect) are checked against the OSV.dev vulnerability database, like npm/PyPI/Maven. ## Output Formats diff --git a/isitsecure/engine/code_analysis/osv_dependency_scanner.py b/isitsecure/engine/code_analysis/osv_dependency_scanner.py index 9e22015..dc151be 100644 --- a/isitsecure/engine/code_analysis/osv_dependency_scanner.py +++ b/isitsecure/engine/code_analysis/osv_dependency_scanner.py @@ -53,7 +53,7 @@ class OSVDependencyScanner: """Scans all dependency files against the OSV.dev vulnerability database. Unified scanner for npm (package.json), PyPI (requirements.txt, - pyproject.toml), and Maven/Gradle (pom.xml, build.gradle). + pyproject.toml), Maven/Gradle (pom.xml, build.gradle), and Go (go.mod). """ SCANNER_NAME = "osv_dependency_scanner" @@ -117,9 +117,54 @@ def _extract_all_dependencies(self, repo: RepoSnapshot) -> list[ParsedDependency deps.extend(self._extract_maven(file_path, content)) elif name in ("build.gradle", "build.gradle.kts"): deps.extend(self._extract_gradle(file_path, content)) + elif name == "go.mod": + deps.extend(self._extract_go(file_path, content)) return deps + def _extract_go(self, file_path: str, content: str) -> list[ParsedDependency]: + """Extract Go modules from go.mod (direct and indirect ``require``s). + + Handles both the block form:: + + require ( + github.com/gin-gonic/gin v1.9.1 + golang.org/x/crypto v0.14.0 // indirect + ) + + and the single-line form ``require github.com/x/y v1.2.3``. The + ``module``/``go``/``toolchain``/``replace``/``exclude``/``retract`` + directives are ignored — only versioned requires reach OSV. The leading + ``v`` is stripped to the semver OSV's Go ecosystem compares against + (``+incompatible`` and pseudo-versions are preserved). + """ + deps: list[ParsedDependency] = [] + in_require = False + for line_num, raw in enumerate(content.splitlines(), 1): + code = raw.split("//", 1)[0].strip() # drop `// indirect` etc. + if not code: + continue + if code.startswith("require (") or code == "require(": + in_require = True + continue + if in_require: + if code == ")": + in_require = False + continue + match = re.match(r"^(\S+)\s+(v\S+)$", code) + elif code.startswith("require "): + match = re.match(r"^require\s+(\S+)\s+(v\S+)$", code) + else: + match = None + if match: + deps.append(ParsedDependency( + name=match.group(1), + version=re.sub(r"^v", "", match.group(2)), + ecosystem="Go", + file_path=file_path, line_number=line_num, raw_line=raw.strip(), + )) + return deps + def _extract_npm(self, file_path: str, content: str) -> list[ParsedDependency]: """Extract from package.json.""" import json diff --git a/isitsecure/engine/constants.py b/isitsecure/engine/constants.py index 0f78d6a..69c98f2 100644 --- a/isitsecure/engine/constants.py +++ b/isitsecure/engine/constants.py @@ -674,6 +674,8 @@ class RepoIngestionConfig: "build.gradle.kts", "application.properties", "application.yml", + # Go projects — go.mod feeds OSV dependency scanning + "go.mod", ) # Directories to skip during indexing diff --git a/tests/engine/test_osv_dependency_scanner.py b/tests/engine/test_osv_dependency_scanner.py index 9e52415..5176135 100644 --- a/tests/engine/test_osv_dependency_scanner.py +++ b/tests/engine/test_osv_dependency_scanner.py @@ -105,10 +105,44 @@ def test_multi_ecosystem(self, scanner): "package.json": '{"dependencies":{"next":"13.4.0"}}', "requirements.txt": "django==3.2.0\n", "pom.xml": "ga1.0", + "go.mod": "module x\nrequire github.com/gin-gonic/gin v1.9.1\n", }) deps = scanner._extract_all_dependencies(snapshot) ecosystems = {d.ecosystem for d in deps} - assert ecosystems == {"npm", "PyPI", "Maven"} + assert ecosystems == {"npm", "PyPI", "Maven", "Go"} + + def test_extracts_go_block_and_singleline(self, scanner): + snapshot = _make_snapshot({ + "go.mod": ( + "module example.com/app\n\ngo 1.21\n\n" + "require (\n" + "\tgithub.com/gin-gonic/gin v1.9.1\n" + "\tgolang.org/x/crypto v0.14.0 // indirect\n" + ")\n\n" + "require github.com/dgrijalva/jwt-go v3.2.0+incompatible\n\n" + "replace github.com/foo/bar => github.com/foo/bar v1.0.1\n" + ) + }) + deps = {d.name: d for d in scanner._extract_all_dependencies(snapshot)} + assert set(deps) == { + "github.com/gin-gonic/gin", + "golang.org/x/crypto", + "github.com/dgrijalva/jwt-go", + } + assert all(d.ecosystem == "Go" for d in deps.values()) + # leading v stripped; +incompatible preserved; indirect included + assert deps["github.com/gin-gonic/gin"].version == "1.9.1" + assert deps["github.com/dgrijalva/jwt-go"].version == "3.2.0+incompatible" + + def test_go_skips_directives(self, scanner): + """module/go/toolchain/replace/exclude are not dependencies.""" + snapshot = _make_snapshot({ + "go.mod": ( + "module example.com/app\ngo 1.21\ntoolchain go1.21.5\n" + "exclude github.com/bad/pkg v1.0.0\n" + ) + }) + assert scanner._extract_all_dependencies(snapshot) == [] class TestSeverityExtraction: