# Directory Structure
```
├── .github
│ └── workflows
│ └── go.yml
├── .gitignore
├── go.mod
├── go.sum
├── main.go
├── Makefile
├── mcp.json-template
├── README.md
├── screenshots
│ ├── mcp-output-1.png
│ ├── mcp-output-2.png
│ └── mcp-output-3.png
└── supply-chain-security-check.mdc
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | bin/*
2 | mcp-osv
3 | *.db
4 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Security Analyst
2 |
3 | [](https://github.com/gleicon/mcp-osv/actions/workflows/go.yml)
4 |
5 | A Model Context Protocol (MCP) server that provides security analysis capabilities by integrating with OSV.dev and AI models to help identify and analyze potential vulnerabilities in your codebase.
6 |
7 | ## Features
8 |
9 | - Vulnerability checking using OSV.dev database
10 | - Basic security analysis of code files
11 | - Integration with AI models for security insights
12 | - MCP protocol support for seamless integration with various AI tools
13 | - Optional static code analysis using Semgrep (if installed)
14 |
15 | ## Cursor/Cline and other co-pilots IDE supply-chain preventions
16 |
17 | `mcp-osv` is the ideal companion for co-pilot coding. Use the [supply-chain-security-check.mdc] ruleset in this repo or build your own to manage dependencies and reduce risk. See below how to setup your IDE.
18 |
19 |
20 | ## Requirements
21 |
22 | ### Core Requirements
23 | ```bash
24 | make deps
25 | make install
26 | ```
27 |
28 | ### Optional: Semgrep Installation
29 | For enhanced static code analysis, you can install Semgrep:
30 |
31 | #### macOS
32 | ```bash
33 | brew install semgrep
34 | ```
35 |
36 | #### Linux
37 | ```bash
38 | python3 -m pip install semgrep
39 | ```
40 |
41 | #### Other platforms
42 | Visit [Semgrep Installation Guide](https://semgrep.dev/docs/getting-started/) for detailed instructions.
43 |
44 | The MCP server will work without Semgrep installed, but will skip the static analysis portion when analyzing directories.
45 |
46 | ## Installation
47 |
48 | ```bash
49 | make deps
50 | make install
51 | ```
52 |
53 | The mcp-osv command will be installed on PATH and use the stdin/stdout method.
54 |
55 | Configure your LLM to use mcp-osv as an agent.
56 |
57 | For ***Cursor*** use the configuration below on `configuration` -> `MCP` tab:
58 |
59 | ```json
60 | {"mcpServers":{"security_analyst":{"name":"Security Analyst","type":"stdio","command":"/usr/local/bin/mcp-osv"}}}
61 | ```
62 |
63 | If you are using ***Claude*** just configure it under Settings -> Developer using the config below:
64 |
65 | ```json
66 | {
67 | "mcpServers": {
68 | "mcp-osv": {
69 | "command": "/usr/local/bin/mcp-osv",
70 | "args": []
71 | }
72 | }
73 | }
74 | ```
75 |
76 | 1. The server provides the following tools:
77 |
78 | ### check_vulnerabilities
79 |
80 | Check for known vulnerabilities in dependencies using OSV.dev database.
81 |
82 | Parameters:
83 |
84 | - `package_name`: Name of the package to check
85 | - `version`: Version of the package to check
86 |
87 | ### analyze_security
88 |
89 | Analyze code for potential security issues based on https://osv.dev - a comprehensive database of open-source vulnerabilities.
90 |
91 | Parameters:
92 |
93 | - `file_path`: Path to the file to analyze
94 |
95 | ## Integration with AI Models
96 |
97 | This server is designed to work with AI models like Claude and Cursor through the MCP protocol. The AI models can use the provided tools to:
98 |
99 | 1. Check dependencies for known vulnerabilities
100 | 2. Analyze code for security issues
101 | 3. Provide recommendations for security improvements
102 |
103 | ## Connecting with Cursor
104 |
105 | ### Sample output
106 | 
107 | 
108 | 
109 |
110 | ### Usage
111 |
112 | See mcp.json-template for an example that works with Cursor IDE.
113 |
114 | After the setup, restart and ask something like "Analyze the security of my project using mcp-osv".
115 |
116 | To Debug in VSCode go to Help -> Toggle developer tools and at the console look for mcp.
117 |
118 | To test the security analysis capabilities:
119 |
120 |
121 | ```bash
122 | # Check for vulnerabilities in a package
123 | "Check for vulnerabilities in the package 'express' version '4.17.1'"
124 |
125 | # Analyze a specific file
126 | "Analyze the security of the file 'main.go'"
127 | ```
128 |
129 | The server will process your requests and provide security insights through the MCP protocol.
130 |
131 |
132 | ## Connect to Claude
133 |
134 | Edit the config file and add the following section (that's the whole file, consider the mcp_osv section if you already have other tools installed.)
135 |
136 | ```json
137 | {
138 | "mcpServers": {
139 | "mcp_osv": {
140 | "command": "/usr/local/bin/mcp-osv",
141 | "args": []
142 | }
143 | }
144 | }
145 | ````
146 |
147 | ## Development
148 |
149 | To add new security analysis capabilities:
150 |
151 | 1. Create a new tool using `mcp.NewTool`
152 | 2. Implement the tool handler
153 | 3. Add the tool to the server using `s.AddTool`
154 | 4. check <https://github.com/mark3labs/mcp-go> for a comprehensive framework to build MCPs in Go.
155 |
156 | ## License
157 |
158 | MIT
159 |
```
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
```yaml
1 | # This workflow will build a golang project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
3 |
4 | name: Go
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | jobs:
13 |
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v4
21 | with:
22 | go-version: '1.24'
23 |
24 | - name: Build
25 | run: go build -v ./...
26 |
27 | - name: Test
28 | run: go test -v ./...
29 |
```
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
```go
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net/http"
10 | "os"
11 | "os/exec"
12 | "path/filepath"
13 | "regexp"
14 | "strings"
15 | "sync"
16 | "time"
17 |
18 | "github.com/mark3labs/mcp-go/mcp"
19 | "github.com/mark3labs/mcp-go/server"
20 | "golang.org/x/time/rate"
21 | )
22 |
23 | // OSVResponse represents the response from OSV.dev API
24 | type OSVResponse struct {
25 | Vulns []struct {
26 | ID string `json:"id"`
27 | Summary string `json:"summary"`
28 | Details string `json:"details"`
29 | Affected []struct {
30 | Package struct {
31 | Name string `json:"name"`
32 | } `json:"package"`
33 | } `json:"affected"`
34 | } `json:"vulns"`
35 | }
36 |
37 | // RateLimiter implements a simple rate limiter for API calls
38 | type RateLimiter struct {
39 | limiter *rate.Limiter
40 | mu sync.Mutex
41 | }
42 |
43 | func NewRateLimiter(rps float64) *RateLimiter {
44 | return &RateLimiter{
45 | limiter: rate.NewLimiter(rate.Limit(rps), 1),
46 | }
47 | }
48 |
49 | func (r *RateLimiter) Wait(ctx context.Context) error {
50 | r.mu.Lock()
51 | defer r.mu.Unlock()
52 | return r.limiter.Wait(ctx)
53 | }
54 |
55 | // Global rate limiter for OSV API calls (1 request per second)
56 | var osvRateLimiter = NewRateLimiter(1)
57 |
58 | // HTTP client with timeout
59 | var httpClient = &http.Client{
60 | Timeout: 10 * time.Second,
61 | Transport: &http.Transport{
62 | MaxIdleConns: 10,
63 | IdleConnTimeout: 30 * time.Second,
64 | DisableCompression: true,
65 | TLSHandshakeTimeout: 5 * time.Second,
66 | },
67 | }
68 |
69 | // Input validation functions
70 | func isValidPackageName(name string) bool {
71 | return regexp.MustCompile(`^[a-zA-Z0-9\-\./]+$`).MatchString(name)
72 | }
73 |
74 | func isValidVersion(version string) bool {
75 | return regexp.MustCompile(`^[a-zA-Z0-9\-\./]+$`).MatchString(version)
76 | }
77 |
78 | func isSafePath(path string) (bool, string) {
79 | // Clean the path
80 | cleanPath := filepath.Clean(path)
81 |
82 | // Check for path traversal attempts
83 | if strings.Contains(cleanPath, "..") {
84 | return false, "path contains directory traversal patterns - .."
85 | }
86 | // Check for path traversal attempts
87 | if strings.HasPrefix(cleanPath, ".") {
88 | return false, "path contains directory traversal patterns - ."
89 | }
90 | // Check if the path exists
91 | _, err := os.Stat(cleanPath)
92 | if err != nil {
93 | if os.IsNotExist(err) {
94 | return false, fmt.Sprintf("path does not exist: %s", cleanPath)
95 | }
96 | return false, fmt.Sprintf("error accessing path: %v", err)
97 | }
98 |
99 | // Path exists and is accessible
100 | return true, ""
101 | }
102 |
103 | // SemgrepResult represents the JSON output from semgrep
104 | type SemgrepResult struct {
105 | Results []struct {
106 | CheckID string `json:"check_id"`
107 | Path string `json:"path"`
108 | Start struct {
109 | Line int `json:"line"`
110 | } `json:"start"`
111 | End struct {
112 | Line int `json:"line"`
113 | } `json:"end"`
114 | Extra struct {
115 | Message string `json:"message"`
116 | Severity string `json:"severity"`
117 | } `json:"extra"`
118 | } `json:"results"`
119 | }
120 |
121 | var semgrepAvailable bool
122 |
123 | func init() {
124 | // Check if semgrep is available
125 | _, err := exec.LookPath("semgrep")
126 | semgrepAvailable = err == nil
127 | if !semgrepAvailable {
128 | log.Println("Semgrep not found in PATH. Static code analysis will be limited.")
129 | }
130 | }
131 |
132 | func runSemgrep(path string) (string, error) {
133 | if !semgrepAvailable {
134 | return "\n=== Semgrep Analysis ===\n⚠️ Semgrep not installed. Static code analysis skipped.\nInstall Semgrep for enhanced security analysis.\n", nil
135 | }
136 |
137 | var result strings.Builder
138 | result.WriteString("\n=== Semgrep Analysis ===\n")
139 |
140 | cmd := exec.Command("semgrep",
141 | "--config=auto",
142 | "--json",
143 | "--severity=WARNING",
144 | "--quiet",
145 | path)
146 |
147 | output, err := cmd.Output()
148 | if err != nil {
149 | if exitErr, ok := err.(*exec.ExitError); ok && len(exitErr.Stderr) > 0 {
150 | return "", fmt.Errorf("semgrep error: %s", exitErr.Stderr)
151 | }
152 | return "", fmt.Errorf("failed to run semgrep: %v", err)
153 | }
154 |
155 | var semgrepResult SemgrepResult
156 | if err := json.Unmarshal(output, &semgrepResult); err != nil {
157 | return "", fmt.Errorf("failed to parse semgrep output: %v", err)
158 | }
159 |
160 | // Add information about what was scanned
161 | result.WriteString("Running Semgrep security analysis...\n")
162 |
163 | if len(semgrepResult.Results) == 0 {
164 | result.WriteString("✅ No security issues found by Semgrep\n")
165 | return result.String(), nil
166 | }
167 |
168 | result.WriteString(fmt.Sprintf("Found %d potential security issues:\n\n", len(semgrepResult.Results)))
169 | for _, finding := range semgrepResult.Results {
170 | result.WriteString(fmt.Sprintf("⚠️ %s\n", finding.CheckID))
171 | result.WriteString(fmt.Sprintf(" Severity: %s\n", finding.Extra.Severity))
172 | result.WriteString(fmt.Sprintf(" File: %s (lines %d-%d)\n", finding.Path, finding.Start.Line, finding.End.Line))
173 | result.WriteString(fmt.Sprintf(" Message: %s\n\n", finding.Extra.Message))
174 | }
175 |
176 | return result.String(), nil
177 | }
178 |
179 | func main() {
180 | // Configure logging
181 | log.SetFlags(log.Ldate | log.Ltime | log.LUTC | log.Lshortfile)
182 | log.Println("Starting MCP Security Analyst server...")
183 |
184 | // Create MCP server
185 | s := server.NewMCPServer(
186 | "Security Analyst MCP",
187 | "1.0.0",
188 | )
189 |
190 | log.Println("Server created, adding tools...")
191 |
192 | // Add OSV vulnerability check tool
193 | osvTool := mcp.NewTool("check_vulnerabilities",
194 | mcp.WithDescription("Check for known vulnerabilities in dependencies"),
195 | mcp.WithString("package_name",
196 | mcp.Required(),
197 | mcp.Description("Name of the package to check"),
198 | ),
199 | mcp.WithString("version",
200 | mcp.Required(),
201 | mcp.Description("Version of the package to check"),
202 | ),
203 | )
204 |
205 | s.AddTool(osvTool, checkVulnerabilitiesHandler)
206 | log.Println("Added check_vulnerabilities tool")
207 |
208 | // Add security analysis tool
209 | analysisTool := mcp.NewTool("analyze_security",
210 | mcp.WithDescription("Analyze code for potential security issues in files or directories"),
211 | mcp.WithString("file_path",
212 | mcp.Required(),
213 | mcp.Description("Path to the file or directory to analyze"),
214 | ),
215 | )
216 |
217 | s.AddTool(analysisTool, analyzeSecurityHandler)
218 | log.Println("Added analyze_security tool")
219 |
220 | log.Println("Starting stdio server...")
221 | // Start the stdio server
222 | if err := server.ServeStdio(s); err != nil {
223 | log.Printf("Server error: %v\n", err)
224 | os.Exit(1)
225 | }
226 | }
227 |
228 | func checkVulnerabilitiesHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
229 | log.Printf("Received check_vulnerabilities request: %+v\n", request)
230 |
231 | // Input validation
232 | pkgName, ok := request.Params.Arguments["package_name"].(string)
233 | if !ok {
234 | return nil, fmt.Errorf("package_name must be a string")
235 | }
236 | if !isValidPackageName(pkgName) {
237 | return nil, fmt.Errorf("invalid package name format")
238 | }
239 |
240 | version, ok := request.Params.Arguments["version"].(string)
241 | if !ok {
242 | return nil, fmt.Errorf("version must be a string")
243 | }
244 | if !isValidVersion(version) {
245 | return nil, fmt.Errorf("invalid version format")
246 | }
247 |
248 | // Rate limiting
249 | if err := osvRateLimiter.Wait(ctx); err != nil {
250 | return nil, fmt.Errorf("rate limit exceeded: %v", err)
251 | }
252 |
253 | // Query OSV.dev API with sanitized inputs
254 | url := fmt.Sprintf("https://api.osv.dev/v1/query?package=%s&version=%s",
255 | strings.ReplaceAll(pkgName, " ", "+"),
256 | strings.ReplaceAll(version, " ", "+"))
257 |
258 | req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
259 | if err != nil {
260 | return nil, fmt.Errorf("failed to create request: %v", err)
261 | }
262 |
263 | resp, err := httpClient.Do(req)
264 | if err != nil {
265 | return nil, fmt.Errorf("failed to query OSV API: %v", err)
266 | }
267 | defer resp.Body.Close()
268 |
269 | body, err := io.ReadAll(resp.Body)
270 | if err != nil {
271 | return nil, fmt.Errorf("failed to read OSV response: %v", err)
272 | }
273 |
274 | var osvResp OSVResponse
275 | if err := json.Unmarshal(body, &osvResp); err != nil {
276 | return nil, fmt.Errorf("failed to parse OSV response: %v", err)
277 | }
278 |
279 | // Format response
280 | var result string
281 | if len(osvResp.Vulns) == 0 {
282 | result = fmt.Sprintf("No known vulnerabilities found for %s@%s", pkgName, version)
283 | } else {
284 | result = fmt.Sprintf("Found %d vulnerabilities for %s@%s:\n", len(osvResp.Vulns), pkgName, version)
285 | for _, vuln := range osvResp.Vulns {
286 | result += fmt.Sprintf("- %s: %s\n", vuln.ID, vuln.Summary)
287 | if vuln.Details != "" {
288 | result += fmt.Sprintf(" Details: %s\n", vuln.Details)
289 | }
290 | }
291 | }
292 |
293 | log.Printf("Completed vulnerability check for %s@%s\n", pkgName, version)
294 | return mcp.NewToolResultText(result), nil
295 | }
296 |
297 | func analyzeSecurityHandler(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
298 | log.Printf("Received analyze_security request: %+v\n", request)
299 |
300 | // Input validation
301 | path, ok := request.Params.Arguments["file_path"].(string)
302 | if !ok {
303 | return nil, fmt.Errorf("file_path must be a string")
304 | }
305 |
306 | // Clean and evaluate the path
307 | cleanPath := filepath.Clean(path)
308 | absPath, err := filepath.Abs(cleanPath)
309 | if err != nil {
310 | return nil, fmt.Errorf("failed to get absolute path: %v", err)
311 | }
312 |
313 | // Check if path exists and get info
314 | fileInfo, err := os.Stat(absPath)
315 | if err != nil {
316 | if os.IsNotExist(err) {
317 | return nil, fmt.Errorf("path does not exist: %s", absPath)
318 | }
319 | return nil, fmt.Errorf("error accessing path: %v", err)
320 | }
321 |
322 | // Path safety check
323 | if strings.Contains(absPath, "..") {
324 | return nil, fmt.Errorf("unsafe path: contains directory traversal patterns")
325 | }
326 |
327 | var result strings.Builder
328 | if fileInfo.IsDir() {
329 | // Handle directory
330 | result.WriteString(fmt.Sprintf("Security analysis for directory: %s\n\n", absPath))
331 |
332 | // Run Semgrep analysis for the directory
333 | semgrepResult, semgrepErr := runSemgrep(absPath)
334 | if semgrepErr != nil {
335 | result.WriteString(fmt.Sprintf("Error running Semgrep: %v\n", semgrepErr))
336 | } else {
337 | result.WriteString(semgrepResult)
338 | }
339 |
340 | // Walk through the directory
341 | err := filepath.Walk(absPath, func(path string, info os.FileInfo, err error) error {
342 | if err != nil {
343 | result.WriteString(fmt.Sprintf("Error accessing %s: %v\n", path, err))
344 | return nil // Continue walking
345 | }
346 |
347 | // Skip directories themselves
348 | if info.IsDir() {
349 | return nil
350 | }
351 |
352 | // Skip non-analyzable files
353 | if !isAnalyzableFile(path) {
354 | return nil
355 | }
356 |
357 | // Analyze each file
358 | fileResult, err := analyzeFile(path)
359 | if err != nil {
360 | result.WriteString(fmt.Sprintf("Error analyzing %s: %v\n", path, err))
361 | return nil // Continue walking
362 | }
363 |
364 | result.WriteString(fmt.Sprintf("\n=== %s ===\n", path))
365 | result.WriteString(fileResult)
366 | result.WriteString("\n")
367 |
368 | return nil
369 | })
370 |
371 | if err != nil {
372 | return nil, fmt.Errorf("error walking directory: %v", err)
373 | }
374 | } else {
375 | // Handle single file
376 | fileResult, err := analyzeFile(absPath)
377 | if err != nil {
378 | return nil, fmt.Errorf("error analyzing file: %v", err)
379 | }
380 | result.WriteString(fileResult)
381 | }
382 |
383 | return mcp.NewToolResultText(result.String()), nil
384 | }
385 |
386 | func analyzeFile(filePath string) (string, error) {
387 | var result strings.Builder
388 | result.WriteString(fmt.Sprintf("File: %s\n", filePath))
389 |
390 | // Read the file
391 | content, err := os.ReadFile(filePath)
392 | if err != nil {
393 | return "", fmt.Errorf("failed to read file: %v", err)
394 | }
395 |
396 | contentStr := string(content)
397 |
398 | // Check for potential security issues
399 | checks := map[string]struct {
400 | pattern string
401 | desc string
402 | }{
403 | "Hardcoded credentials": {
404 | pattern: `(?i)(password|secret|key|token|auth).*[=:]\s*['"][^'"]+['"]`,
405 | desc: "Potential hardcoded credentials found",
406 | },
407 | "SQL injection risk": {
408 | pattern: `(?i)(db\.Query|db\.Exec|sql\.Open)\s*\(\s*([^,]+\+|fmt\.Sprintf).*?(SELECT|INSERT|UPDATE|DELETE)`,
409 | desc: "Possible SQL injection risk - using string concatenation or formatting in database query",
410 | },
411 | "Insecure HTTP": {
412 | pattern: `http://[^/]*\.[^/]*`,
413 | desc: "Insecure HTTP URL found (not HTTPS)",
414 | },
415 | "Command execution": {
416 | pattern: `exec\.(Command|CommandContext)`,
417 | desc: "Command execution detected - validate inputs carefully",
418 | },
419 | "Hardcoded IPs": {
420 | pattern: `\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b`,
421 | desc: "Hardcoded IP address found - consider configuration",
422 | },
423 | }
424 |
425 | foundIssues := false
426 | for checkName, check := range checks {
427 | if matched, err := regexp.MatchString(check.pattern, contentStr); err == nil && matched {
428 | result.WriteString(fmt.Sprintf("⚠️ %s\n %s\n", checkName, check.desc))
429 | foundIssues = true
430 | }
431 | }
432 |
433 | // File-specific checks
434 | switch filepath.Base(filePath) {
435 | case "go.mod":
436 | // Check dependencies in go.mod
437 | deps, err := parseGoMod(contentStr)
438 | if err != nil {
439 | result.WriteString(fmt.Sprintf("Error parsing go.mod: %v\n", err))
440 | } else {
441 | result.WriteString("\nDependency analysis:\n")
442 | for _, dep := range deps {
443 | // Check each dependency with OSV
444 | vulns, err := checkOSVVulnerabilities(context.Background(), dep.name, dep.version)
445 | if err != nil {
446 | result.WriteString(fmt.Sprintf("Error checking %s@%s: %v\n", dep.name, dep.version, err))
447 | continue
448 | }
449 | if len(vulns) > 0 {
450 | result.WriteString(fmt.Sprintf("⚠️ %s@%s has %d known vulnerabilities:\n", dep.name, dep.version, len(vulns)))
451 | for _, vuln := range vulns {
452 | result.WriteString(fmt.Sprintf(" - %s: %s\n", vuln.ID, vuln.Summary))
453 | }
454 | foundIssues = true
455 | }
456 | }
457 | }
458 | }
459 |
460 | if !foundIssues {
461 | result.WriteString("✅ No immediate security concerns found\n")
462 | }
463 |
464 | return result.String(), nil
465 | }
466 |
467 | type dependency struct {
468 | name string
469 | version string
470 | }
471 |
472 | func parseGoMod(content string) ([]dependency, error) {
473 | var deps []dependency
474 | lines := strings.Split(content, "\n")
475 | for _, line := range lines {
476 | line = strings.TrimSpace(line)
477 | if strings.HasPrefix(line, "require ") || (len(line) > 0 && line[0] != ' ' && strings.Contains(line, " v")) {
478 | parts := strings.Fields(line)
479 | if len(parts) >= 2 {
480 | deps = append(deps, dependency{
481 | name: parts[0],
482 | version: parts[1],
483 | })
484 | }
485 | }
486 | }
487 | return deps, nil
488 | }
489 |
490 | func checkOSVVulnerabilities(ctx context.Context, pkgName, version string) ([]struct{ID, Summary string}, error) {
491 | // Rate limiting
492 | if err := osvRateLimiter.Wait(ctx); err != nil {
493 | return nil, fmt.Errorf("rate limit exceeded: %v", err)
494 | }
495 |
496 | // Query OSV.dev API
497 | url := fmt.Sprintf("https://api.osv.dev/v1/query?package=%s&version=%s",
498 | strings.ReplaceAll(pkgName, " ", "+"),
499 | strings.ReplaceAll(version, " ", "+"))
500 |
501 | req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
502 | if err != nil {
503 | return nil, fmt.Errorf("failed to create request: %v", err)
504 | }
505 |
506 | resp, err := httpClient.Do(req)
507 | if err != nil {
508 | return nil, fmt.Errorf("failed to query OSV API: %v", err)
509 | }
510 | defer resp.Body.Close()
511 |
512 | body, err := io.ReadAll(resp.Body)
513 | if err != nil {
514 | return nil, fmt.Errorf("failed to read OSV response: %v", err)
515 | }
516 |
517 | var osvResp OSVResponse
518 | if err := json.Unmarshal(body, &osvResp); err != nil {
519 | return nil, fmt.Errorf("failed to parse OSV response: %v", err)
520 | }
521 |
522 | var vulns []struct{ID, Summary string}
523 | for _, v := range osvResp.Vulns {
524 | vulns = append(vulns, struct{ID, Summary string}{
525 | ID: v.ID,
526 | Summary: v.Summary,
527 | })
528 | }
529 | return vulns, nil
530 | }
531 |
532 | func isAnalyzableFile(path string) bool {
533 | // List of file extensions to analyze
534 | analyzableExts := map[string]bool{
535 | ".go": true,
536 | ".mod": true,
537 | ".sum": true,
538 | ".json": true,
539 | ".yaml": true,
540 | ".yml": true,
541 | ".toml": true,
542 | ".env": true,
543 | }
544 |
545 | ext := strings.ToLower(filepath.Ext(path))
546 | base := filepath.Base(path)
547 |
548 | // Special files
549 | if base == "go.mod" || base == "go.sum" {
550 | return true
551 | }
552 |
553 | return analyzableExts[ext]
554 | }
```