Skip to content

🔒 security_advisories — Security Advisory Query

This example demonstrates how to fetch security vulnerability advisories for Composer packages, covering both "incremental pull by time" and "targeted query by package name," and prints key fields like CVE and affected versions.

🎯 Example Positioning

security_advisories is the 5th example in the Packagist API Remote Operations series, rated "intermediate" difficulty. After mastering basic client initialization, package listing, and statistics queries, this example turns the view toward the security dimension — an unavoidable part of any serious PHP project.

  • 📚 What you'll learn: how to use the underlying methods of the pkg/repository package to query Packagist's disclosed security advisories (CVE / GHSA) along two dimensions — "update time" and "package name" — and parse the core fields of the Advisory struct.
  • 🔗 Corresponding SDK methods: repository.Repository.ListSecurityAdvisories (incremental by time) and repository.Repository.ListAdvisories (query by package name).
  • 🧭 Difference from security_monitor: security_monitor is a comprehensive hands-on example that chains advisory queries, version-constraint comparison, and alert notifications into a monitoring pipeline; this example focuses solely on how to pull advisory data back and understand it, serving as the prerequisite course for security_monitor.
  • ⚖️ Difference from Composer CLI audit: CLI audit audits the actually locked versions in the local composer.lock and requires PHP; this example queries the Packagist全库 advisories — pure HTTP, no PHP required — suitable for SCA panoramic scanning and vulnerability database syncing.

💻 Full Code

go
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/scagogogo/composer-skills/pkg/repository"
)

func main() {
	// Example 5: Fetch security advisories for Composer packages
	// Includes both by-time and by-package-name fetching, usable for security audits.

	// Step 1: Initialize the repository client
	options := &repository.Options{
		ServerUrl: "https://packagist.org", // Use the official repository
	}
	repo := &repository.Repository{}
	_ = options // In this example, only a config placeholder; the methods internally use the official endpoint
	ctx := context.Background()

	// Step 2: Fetch security advisories by time
	fmt.Println("=== Fetch security advisories by time ===")
	oneYearAgo := time.Now().AddDate(-1, 0, 0)
	fmt.Printf("Fetching security advisories since %s...\n", oneYearAgo.Format("2006-01-02"))

	advisoriesResp, err := repo.ListSecurityAdvisories(ctx, oneYearAgo)
	if err != nil {
		fmt.Printf("Failed to fetch security advisories: %v\n", err)
		return
	}

	// Count the advisories
	totalAdvisories := 0
	for _, advisories := range advisoriesResp.Advisories {
		totalAdvisories += len(advisories)
	}
	fmt.Printf("Found %d packages with security advisories, %d advisories in total\n",
		len(advisoriesResp.Advisories), totalAdvisories)

	// Print partial advisory details (first 3 packages, first 2 advisories each)
	count := 0
	fmt.Println("\nPartial security advisory details:")
	for pkgName, advisories := range advisoriesResp.Advisories {
		if count >= 3 {
			break
		}
		fmt.Printf("\nPackage: %s\n", pkgName)
		for i, advisory := range advisories {
			if i >= 2 {
				fmt.Printf("  ...%d more advisories not shown\n", len(advisories)-i)
				break
			}
			fmt.Printf("  - Title: %s\n", advisory.Title)
			fmt.Printf("    CVE: %s\n", advisory.Cve)
			fmt.Printf("    Reported at: %s\n", advisory.ReportedAt)
			fmt.Printf("    Affected versions: %s\n", advisory.AffectedVersions)
		}
		count++
	}

	// Step 3: Fetch security advisories for a specific package
	fmt.Println("\n\n=== Fetch security advisories for a specific package ===")
	packageName := "symfony/http-kernel"
	fmt.Printf("Fetching security advisories for %s...\n", packageName)

	packageAdvisories, err := repo.ListAdvisories(ctx, packageName)
	if err != nil {
		fmt.Printf("Failed to fetch security advisories for %s: %v\n", packageName, err)
	} else {
		fmt.Printf("Found %d security advisories\n", len(packageAdvisories))
		fmt.Println("\nSecurity advisory details:")
		for i, advisory := range packageAdvisories {
			if i >= 5 {
				fmt.Printf("...%d more advisories not shown\n", len(packageAdvisories)-i)
				break
			}
			fmt.Printf("\n%d. %s\n", i+1, advisory.Title)
			fmt.Printf("   - Advisory ID: %s\n", advisory.AdvisoryID)
			fmt.Printf("   - CVE: %s\n", advisory.Cve)
			fmt.Printf("   - Reported at: %s\n", advisory.ReportedAt)
			fmt.Printf("   - Affected versions: %s\n", advisory.AffectedVersions)
			fmt.Printf("   - Link: %s\n", advisory.Link)
		}
	}
}

🧩 Code Walkthrough

  • 🏗️ Initialize Repository: repo := &repository.Repository{} constructs the underlying HTTP client with a zero value. The options field is unexported, and the methods internally always request the official https://packagist.org endpoint, so in this example options is only a config placeholder (_ = options).
  • 🕐 Incremental pull by time: oneYearAgo := time.Now().AddDate(-1, 0, 0) constructs a time point one year ago and passes it to repo.ListSecurityAdvisories(ctx, oneYearAgo). The SDK takes its millisecond-level UnixMilli() timestamp as the updatedSince query parameter and returns all advisories updated since that time.
  • 🗺️ Understanding the return structure: advisoriesResp.Advisories is map[string][]*Advisory — the key is the package name (e.g. symfony/http-kernel), and the value is the list of advisories for that package. The outer for iterates packages, the inner for iterates that package's advisories, and summing each package's slice length gives the total count.
  • 📝 Key field interpretation: each Advisory prints Title, Cve (CVE ID), ReportedAt (report time), and AffectedVersions (affected version range, in Composer constraint syntax like >=4.4.0,<4.4.44||>=5.0.0,<5.4.15). These fields are the core basis for deciding "whether you're affected."
  • 🎯 Targeted query by package name: repo.ListAdvisories(ctx, "symfony/http-kernel") takes a package name directly and returns a flat []*Advisory slice (no longer a map). It hits the same endpoint but uses packages[]=symfony/http-kernel as the query parameter — suitable for directed checks on a single dependency in a CI gate.
  • ✂️ Truncated output: the example uses count >= 3 and i >= 2 / i >= 5 to truncate output, avoiding screen flooding when there are many advisories — in production code, replace this with persistent storage or structured logging.
  • 🛡️ Error handling: on by-time pull failure, return directly (subsequent steps are meaningless); on by-package-name query failure, only print a warning and continue, demonstrating a "non-fatal error" tolerance strategy. In practice, adjust this based on business importance.

▶️ How to Run

Run directly from the example directory:

bash
cd /home/cc11001100/github/scagogogo/composer-skills/examples/security_advisories
go run main.go

Or run from the repository root:

bash
go run examples/security_advisories/main.go

Pure Go, no PHP required

This example only calls the Packagist HTTP API — no local PHP or Composer needed. Just clone and go run.

⚠️ This example makes real Packagist API calls; avoid running it at high frequency. The by-time response can be large (tens of MB), so ensure adequate network and memory. For long-term monitoring, switch to GetSecurityAdvisoriesSince with a persisted "last sync time" for incremental pulls.

📚 SDK Methods Involved

Method NamePackageEndpoint / Query ParamsDoc Link
ListSecurityAdvisoriespkg/repositoryGET /api/security-advisories/?updatedSince={ms}/sdk/packagist/repository#listsecurityadvisories
ListAdvisoriespkg/repositoryGET /api/security-advisories/?packages[]={name}/sdk/packagist/repository#listadvisories

📝 Note: These two methods belong to the lower Repository layer and accept context.Context for propagating timeout and cancellation. The higher-level facade pkg/client.ComposerClient provides an equivalent trio — GetSecurityAdvisories (full), GetSecurityAdvisoriesSince (by time, second-level timestamp), and GetSecurityAdvisoriesForPackages (by package-name list); see /sdk/packagist/advisories. Both hit the same endpoint — they differ only in timestamp precision and parameter wrapping, so choose based on your semantic needs.

🚀 Going Further

  • ⏱️ Add timeout control: replace context.Background() with ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) and defer cancel() to prevent the network from hanging during a full pull.
  • 🗄️ Persist to a database: serialize AdvisoriesResponse.Advisories into a database (upsert by AdvisoryID for dedup), and build a time index on ReportedAt to construct your own vulnerability knowledge base.
  • 🔁 Incremental sync: after each query, store time.Now() as a "checkpoint"; next time, use it as the updatedSince parameter to pull only new/updated advisories, avoiding repeated full requests.
  • 🎯 Version-constraint comparison: after getting AffectedVersions (e.g. >=5.4.0,<5.4.19||>=6.0.0,<6.0.4), do constraint matching against the versions actually installed in your project's composer.lock to determine whether you're truly affected — this is exactly the core problem the security_monitor example solves.
  • 🔔 Hook up notification channels: when new advisories are discovered, push them to Slack / Feishu / email, or write them into a to-do system, forming a "discover → notify → fix" closed loop.
  • 🧪 CI gate: in your CI pipeline, call ListAdvisories against the dependency list in composer.lock and block the build on any hit, shifting security left to the commit stage.

Released under the MIT License