Создание сканера зависимостей GO с нуля

Создание сканера зависимостей GO с нуля

19 августа 2025 г.

При управлении проектами GO необходимо отслеживать зависимости, проверить на наличие уязвимостей и обеспечить соответствие лицензии. Вместо того, чтобы полагаться на внешние инструменты, давайте создадим наш собственный анализатор зависимости, используя стандартную библиотеку GO.

Основная структура

Мы будем работать с модулями GO, поэтому нам нужны структуры, чтобы представлять их:

package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"regexp"
	"sort"
	"strings"
	"time"
)

type Module struct {
	Path     string
	Version  string
	Indirect bool
}

type GoMod struct {
	Module   Module
	Requires []Module
}

Наш инструмент будет обрабатывать три операции: перечисление зависимостей, сканирование уязвимости и проверка лицензий.

Расположение файлов go.mod

Понимание структуры файла модуля

Файл go.mod использует определенный формат, который нам нужно правильно проанализировать. Объявления модуля начинаются с ключевого слова модуля, за которым следует путь модуля. Зависимости перечислены в требованиях, которые могут быть однострочными или сгруппированы в многострочные блоки.

Логика анализа обрабатывает оба формата, отслеживая, есть ли мы в многослойном потреблении блока. Мы используем регулярные выражения для извлечения пути и версии модуля из каждой строки, и обнаруживаем косвенные зависимости, ища // непрямой комментарий. Этот подход дает нам ту же информацию, что и список Go, но без нереста внешних процессов.

Вместо того, чтобы выходить наgo list, мы можем проанализироватьgo.modфайл напрямую:

func parseGoMod() (*GoMod, error) {
	file, err := os.Open("go.mod")
	if err != nil {
		return nil, fmt.Errorf("go.mod not found: %v", err)
	}
	defer file.Close()

	goMod := &GoMod{
		Requires: []Module{},
	}

	scanner := bufio.NewScanner(file)
	inRequire := false
	requireRegex := regexp.MustCompile(`^\s*([^\s]+)\s+([^\s]+)(?:\s+//\s*indirect)?`)
	moduleRegex := regexp.MustCompile(`^module\s+(.+)`)

	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())

		if strings.HasPrefix(line, "module ") {
			if matches := moduleRegex.FindStringSubmatch(line); len(matches) > 1 {
				goMod.Module = Module{Path: matches[1]}
			}
		}

		if strings.HasPrefix(line, "require (") {
			inRequire = true
			continue
		}

		if inRequire && line == ")" {
			inRequire = false
			continue
		}

		if inRequire || strings.HasPrefix(line, "require ") {
			cleanLine := strings.TrimPrefix(line, "require ")
			if matches := requireRegex.FindStringSubmatch(cleanLine); len(matches) >= 3 {
				module := Module{
					Path:     matches[1],
					Version:  matches[2],
					Indirect: strings.Contains(line, "indirect"),
				}
				goMod.Requires = append(goMod.Requires, module)
			}
		}
	}

	return goMod, scanner.Err()
}

Сигсерчик обрабатывает как однострочные, так и многострочные требуют блоков. Он извлекает пути модуля, версии и идентифицирует косвенные зависимости.

Запросы базы данных уязвимости

Как работает проверка уязвимости

Базы данных уязвимости хранят записи о известных проблемах безопасности в пакетах программного обеспечения. Каждая уязвимость получает назначенные идентификаторы, такие как номера CVE, и включает подробную информацию о затронутых версиях. Процесс работает так: мы отправляем имя и версию пакета в API базы данных, он проверяет, имеет ли эта конкретная версия какие -либо известные уязвимости, а затем возвращает список проблем, если обнаружено.

База данных OSV особенно полезна, потому что она агрегирует данные об уязвимости из нескольких источников и предоставляет унифицированный API. Когда мы запрашиваем это, мы по сути спрашиваем: «Есть ли точная версия этого пакета, зарегистрированные по вопросам безопасности?» База данных выполняет сопоставление версий и возвращает структурированные данные о любых выводах.

Мы можем проверить базу данных OSV (уязвимости с открытым исходным кодом) для известных проблем:

func checkOSVDatabase(modulePath, version string) []string {
	url := "https://api.osv.dev/v1/query"

	payload := map[string]interface{}{
		"package": map[string]string{
			"name":      modulePath,
			"ecosystem": "Go",
		},
		"version": version,
	}

	jsonData, err := json.Marshal(payload)
	if err != nil {
		return []string{}
	}

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Post(url, "application/json", strings.NewReader(string(jsonData)))
	if err != nil {
		return []string{}
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return []string{}
	}

	var result struct {
		Vulns []struct {
			ID      string `json:"id"`
			Summary string `json:"summary"`
		} `json:"vulns"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return []string{}
	}

	var vulnerabilities []string
	for _, vuln := range result.Vulns {
		vulnStr := fmt.Sprintf("%s: %s", vuln.ID, vuln.Summary)
		vulnerabilities = append(vulnerabilities, vulnStr)
	}

	return vulnerabilities
}

func checkVulnerabilities() {
	goMod, err := parseGoMod()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	vulnerableModules := 0

	for i, mod := range goMod.Requires {
		fmt.Printf("\rScanning %d/%d: %s", i+1, len(goMod.Requires), mod.Path)
		vulns := checkOSVDatabase(mod.Path, mod.Version)
		if len(vulns) > 0 {
			vulnerableModules++
			fmt.Printf("\n🚨 %s@%s:\n", mod.Path, mod.Version)
			for _, vuln := range vulns {
				fmt.Printf("  - %s\n", vuln)
			}
		}
	}

	if vulnerableModules == 0 {
		fmt.Println("\n✅ No known vulnerabilities found")
	} else {
		fmt.Printf("\n⚠️ Found %d vulnerable modules\n", vulnerableModules)
	}
}

Проверка уязвимости отправляет полезную нагрузку JSON с именем и версией модуля, а затем анализирует ответ для любых зарегистрированных уязвимостей.

Информация о лицензии

Как работает обнаружение лицензии

Проверка соответствия лицензии включает в себя определение того, какие юридические условия регулируют каждую зависимость в вашем проекте. Большинство проектов с открытым исходным кодом включают файлы лицензий в их репозитории, а также платформы, такие как Github, проповедовать эти файлы, чтобы определить тип лицензии с использованием идентификаторов SPDX.

Our approach leverages GitHub's license detection API, which analyzes repository contents and returns standardized license identifiers. Для модулей, размещенных на GitHub, мы извлекаем имя владельца и хранилище из пути модуля, затем запросим конечную точку API GitHub, которая специально предоставляет информацию о лицензии. This gives us machine-readable license data without having to download and parse license files ourselves.

Различные лицензии имеют разные требования, некоторые вроде MIT очень разрешают, в то время как другие, такие как GPL, имеют требования к копированию, которые могут повлиять на то, как вы можете распространять свое программное обеспечение. Понимание этих различий имеет решающее значение для юридического соблюдения.

Для модулей с GitHub, мы можем получить данные лицензии от их API:

func fetchGitHubLicense(owner, repo string) string {
	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/license", owner, repo)

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Get(url)
	if err != nil {
		return "Unknown"
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		return "Unknown"
	}

	var result struct {
		License struct {
			SPDXID string `json:"spdx_id"`
		} `json:"license"`
	}

	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return "Unknown"
	}

	if result.License.SPDXID != "" && result.License.SPDXID != "NOASSERTION" {
		return result.License.SPDXID
	}

	return "Unknown"
}

func fetchLicenseFromRepo(modulePath string) string {
	if strings.HasPrefix(modulePath, "golang.org/x/") {
		return "BSD-3-Clause"
	}

	if !strings.HasPrefix(modulePath, "github.com/") {
		return "Unknown"
	}

	parts := strings.Split(modulePath, "/")
	if len(parts) < 3 {
		return "Unknown"
	}

	return fetchGitHubLicense(parts[1], parts[2])
}

func checkLicenses() {
	goMod, err := parseGoMod()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	licenseCount := make(map[string]int)

	for i, mod := range goMod.Requires {
		fmt.Printf("\rProcessing %d/%d...", i+1, len(goMod.Requires))
		license := fetchLicenseFromRepo(mod.Path)
		licenseCount[license]++
		fmt.Printf("\r  %s: %s\n", mod.Path, license)
	}

	fmt.Println("\nLicense Distribution:")
	for license, count := range licenseCount {
		percentage := float64(count) / float64(len(goMod.Requires)) * 100
		fmt.Printf("  %s: %d modules (%.1f%%)\n", license, count, percentage)
	}
}

Проверка лицензии признает, чтоgolang.org/x/Пакеты используют BSD-3-Clause, а затем запрашивают API GitHub для других репозиториев.

Анализ зависимостей с проверкой контрольной суммы

Анализатор зависимостей перечисляет модули и проверяет их целостность, используяgo.sum:

func parseGoSum() map[string]string {
	checksums := make(map[string]string)

	file, err := os.Open("go.sum")
	if err != nil {
		return checksums
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		parts := strings.Fields(scanner.Text())
		if len(parts) >= 3 {
			module := parts[0] + "@" + parts[1]
			checksums[module] = parts[2]
		}
	}

	return checksums
}

func analyzeDependencies() {
	goMod, err := parseGoMod()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	checksums := parseGoSum()

	fmt.Printf("Module: %s\n", goMod.Module.Path)
	fmt.Printf("Found %d dependencies:\n\n", len(goMod.Requires))

	direct, indirect := 0, 0

	sort.Slice(goMod.Requires, func(i, j int) bool {
		return goMod.Requires[i].Path < goMod.Requires[j].Path
	})

	for _, mod := range goMod.Requires {
		status := "direct"
		if mod.Indirect {
			status = "indirect"
			indirect++
		} else {
			direct++
		}

		checksumKey := mod.Path + "@" + mod.Version
		hasChecksum := "❌"
		if _, exists := checksums[checksumKey]; exists {
			hasChecksum = "✅"
		}

		fmt.Printf("  %s %s@%s (%s)\n", hasChecksum, mod.Path, mod.Version, status)
	}

	fmt.Printf("\nSummary: %d direct, %d indirect dependencies\n", direct, indirect)
}

Командный интерфейс

Основная функция направляет команды для соответствующих обработчиков:

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Usage: go run main.go <command>")
		fmt.Println("Commands:")
		fmt.Println("  deps      List all dependencies")
		fmt.Println("  vulns     Check for vulnerabilities")
		fmt.Println("  licenses  Check license compliance")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "deps":
		analyzeDependencies()
	case "vulns":
		checkVulnerabilities()
	case "licenses":
		checkLicenses()
	default:
		fmt.Println("Unknown command")
		os.Exit(1)
	}
}

Запуск инструмента

Сохраните код какmain.goИ запустите его в любом проекте GO:

# List dependencies with checksum verification
go run main.go deps

# Scan for vulnerabilities
go run main.go vulns

# Analyze licenses
go run main.go licenses

Вывод показывает информацию о зависимости, отчеты об уязвимости и распределение лицензий по зависимости вашего проекта. Инструмент демонстрирует, как анализ зависимости работает за кулисами, файлами модулей анализа, запроса общедоступных API и источников данных перекрестных ссылок.

Эта реализация охватывает основные концепции, но является лишь отправной точкой. Реальное сканирование уязвимости требует исчерпывающей базы данных, сложного сопоставления диапазона версий, ложной положительной фильтрации и надежной обработки ошибок. Инструменты соответствия лицензии нуждаются в механизмах правовой политики, матрицах совместимости и пользовательском обнаружении лицензий, помимо того, что предоставляет API Github. Для использования в производстве вы хотели бы несколько источников данных, кэширования, ограничения ставок и гораздо более тщательной логики проверки.

Счастливое кодирование;)

Вы можете найти исходный код здесьhttps://github.com/rezmoss/go-preadency-canner


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE