
Создание сканера зависимостей 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
Оригинал