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: