🛡️ security_monitor — Security Monitoring
This example demonstrates how to build a continuously running security monitor based on the Packagist security advisory endpoint: pull all advisories, filter for packages of interest, persist results to timestamped JSON reports, and provide a cron-based scheduling approach.
🎯 Example Positioning
security_monitor is the 7th and most comprehensive example in the Packagist API Remote Operations series. It chains together the client initialization and advisory querying learned earlier into a small, production-ready tool.
- 📚 What you'll learn: how to wrap the SDK's
GetSecurityAdvisoriesinside a custom struct, combined withos/encoding/json/path/filepathto implement a full "pull → filter → persist → scheduled re-run" pipeline. - 🔗 Corresponding SDK method:
client.ComposerClient.GetSecurityAdvisories. - 💡 Difference from
security_advisories:security_advisoriesonly demonstrates "one-off call + print"; this example productionizes it — adding a data directory, timestamped filenames, tracked-package filtering, and cron hints, marking the first step toward a production-grade monitor. - 🧱 Design highlights: the
SecurityMonitorstruct holds three pieces of state — the client, the data directory, and the tracked-packages list. TheFetchAdvisoriesmethod exposes the "run one scan" semantics, making it easy for a scheduler to call repeatedly.
💻 Full Code
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/scagogogo/composer-skills/pkg/client"
"github.com/scagogogo/composer-skills/pkg/domain"
)
// SecurityMonitor represents a security monitor
type SecurityMonitor struct {
client *client.ComposerClient
dataDir string
trackedPackages []string
}
// NewSecurityMonitor creates a new security monitor
func NewSecurityMonitor(dataDir string, packages []string) *SecurityMonitor {
return &SecurityMonitor{
client: client.NewComposerClient(30 * time.Second),
dataDir: dataDir,
trackedPackages: packages,
}
}
// FetchAdvisories fetches and saves security advisories
func (m *SecurityMonitor) FetchAdvisories() error {
// Fetch all security advisories
fmt.Println("Fetching all security advisories...")
advisories, err := m.client.GetSecurityAdvisories()
if err != nil {
return fmt.Errorf("failed to fetch security advisories: %w", err)
}
// Ensure the data directory exists
if err := os.MkdirAll(m.dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory: %w", err)
}
// Save the full advisory data
timestamp := time.Now().Format("20060102-150405")
fullDataPath := filepath.Join(m.dataDir, fmt.Sprintf("all_advisories_%s.json", timestamp))
data, err := json.MarshalIndent(advisories, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize advisory data: %w", err)
}
if err := os.WriteFile(fullDataPath, data, 0644); err != nil {
return fmt.Errorf("failed to save advisory data: %w", err)
}
fmt.Printf("Saved all advisory data to %s\n", fullDataPath)
// Filter advisories for tracked packages
trackedAdvisories := make(map[string][]*domain.Advisory)
for _, pkgName := range m.trackedPackages {
if advisories, ok := advisories.Advisories[pkgName]; ok {
trackedAdvisories[pkgName] = advisories
fmt.Printf("Found %d security advisories for %s\n", len(advisories), pkgName)
}
}
// If tracked packages have advisories, save a separate report
if len(trackedAdvisories) > 0 {
trackedDataPath := filepath.Join(m.dataDir, fmt.Sprintf("tracked_advisories_%s.json", timestamp))
data, err := json.MarshalIndent(trackedAdvisories, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize tracked-package advisory data: %w", err)
}
if err := os.WriteFile(trackedDataPath, data, 0644); err != nil {
return fmt.Errorf("failed to save tracked-package advisory data: %w", err)
}
fmt.Printf("Saved tracked-package advisory data to %s\n", trackedDataPath)
} else {
fmt.Println("No security advisories found for tracked packages")
}
return nil
}
func main() {
// List of packages to track
trackedPackages := []string{
"symfony/symfony",
"laravel/framework",
"guzzlehttp/guzzle",
"monolog/monolog",
"phpunit/phpunit",
}
// Create the security monitor
monitor := NewSecurityMonitor("security_data", trackedPackages)
// Fetch and save security advisories
if err := monitor.FetchAdvisories(); err != nil {
log.Fatalf("Failed to monitor security advisories: %v", err)
}
fmt.Println("\nSecurity monitoring complete. You can set this script to run periodically via a cron job for continuous monitoring of security advisories.")
fmt.Println("Example cron expression (run once a day): 0 0 * * * /path/to/security_monitor")
}🧩 Code Walkthrough
- 🏗️ Encapsulating the monitor struct:
SecurityMonitorcollapses*client.ComposerClient, the data directorydataDir, and the tracked-packages listtrackedPackagesinto a single object. This way, all state needed for "one scan round" is encapsulated, and external callers only needmonitor.FetchAdvisories(), making it easy for cron to schedule repeatedly. - ⏱️ Client with timeout:
client.NewComposerClient(30 * time.Second)injects an HTTP timeout at construction time, preventing the process from hanging for a long time when the advisory endpoint responds slowly. In production, tune this value upward based on data volume. - 🌐 One-shot full pull:
m.client.GetSecurityAdvisories()hits/api/security-advisories/?updatedSince=0and returns*domain.AdvisoriesResponse, whoseAdvisoriesfield ismap[packageName][]*Advisory, convenient for O(1) lookups by package name. - 📁 Idempotent directory creation:
os.MkdirAll(m.dataDir, 0755)does not error when the directory already exists, ensuring consistent behavior between the first run and subsequent re-runs — no need tomkdirmanually in advance. - 🕒 Timestamped filenames:
time.Now().Format("20060102-150405")produces aYYYYMMDD-HHMMSSstring, embedded intoall_advisories_<ts>.jsonandtracked_advisories_<ts>.json. Each run produces a snapshot, so history is traceable and files don't overwrite each other. - 💾 Pretty-printed JSON:
json.MarshalIndent(advisories, "", " ")serializes with two-space indentation; the resulting file can be read directly withjqor a text editor, convenient for manual inspection and diffing. - 🔍 Filter by tracked packages: iterate
trackedPackagesand look up each package name in theadvisories.Advisoriesmap — on a hit, collect it intotrackedAdvisoriesand print the hit count. This is more efficient than a full traversal, since the tracked list is usually far smaller than the full advisory set. - 🧾 Two reports, separated: the full report serves as an audit record; the tracked report contains only the packages of interest — smaller in size, stronger in signal, and suitable for feeding directly into downstream alerting logic.
- 🛡️ Error wrapping and interruption: each step wraps context with
fmt.Errorf("...: %w", err)and ultimately aborts vialog.Fatalfinmain; if any step fails, it won't continue writing dirty data. - ⏰ Cron-ification hint: the tail prints
0 0 * * * /path/to/security_monitor, upgrading "run once" to "run once a day" continuous monitoring — just drop the compiled binary into a crontab.
▶️ How to Run
Run directly from the example directory:
cd /home/cc11001100/github/scagogogo/composer-skills/examples/security_monitor
go run main.goYou can also compile it into a binary and hand it to cron:
go build -o security_monitor
./security_monitor
# Add to crontab to run once at midnight every day
# 0 0 * * * /path/to/security_monitor⚠️ This example makes real Packagist API calls, and the full advisory data is large (MB-scale). Avoid running it at high frequency; when integrating with cron, once a day is recommended, and ensure the working directory has permission to write
security_data/.
📚 SDK Methods Involved
| Method Name | Package | Endpoint | Doc Link |
|---|---|---|---|
GetSecurityAdvisories | pkg/client (ComposerClient) | GET /api/security-advisories/?updatedSince=0 | /sdk/packagist/methods/get-security-advisories |
NewComposerClient | pkg/client | Constructor (injects HTTP timeout) | /sdk/packagist/methods/get-security-advisories |
📝 Note: The core of this example calls only one SDK method,
GetSecurityAdvisories. Thedomain.AdvisoriesResponse.Advisoriesit returns ismap[string][]*domain.Advisory, so subsequent filtering, counting, and serialization are done with the standard library anddomaintypes — no other SDK method needed. If you only need to query a handful of packages, use the more bandwidth-friendlyGetSecurityAdvisoriesForPackages.
🚀 Going Further
- ⏱️ Incremental pulling: swap
GetSecurityAdvisoriesforGetSecurityAdvisoriesSince(lastRunTime)to pull only advisories updated since the last run, drastically reducing traffic and persisted volume; just recordlastRunTimein a local file. - 🎯 Targeted queries: if the number of tracked packages is small, use
GetSecurityAdvisoriesForPackages(trackedPackages)directly — the server filters bypackages[], sparing the local full-set filter. - 📢 Hook up alerting channels: when
trackedAdvisoriesis non-empty, format the hit*domain.Advisoryentries (which include CVE IDs, affected versions, and fixed versions) and push them to a Slack/Feishu webhook or email to achieve "alert on discovery." - 🗃️ History comparison and deduplication: when scanning, read the previous
tracked_advisories_*.jsonand flag new advisory IDs as "NEW" to avoid duplicate alerts; keep snapshots from the last N days and rotate them out automatically. - 🧾 Structured storage: persist
*domain.Advisoryinto SQLite/PostgreSQL, index by package name, severity, and CVE, then build a security posture dashboard on the front end. - 🔁 More robust scheduling: replace cron with a systemd timer or Go's built-in
time.Ticker, add failure retry and exponential backoff, and write alast_run.json(status + timestamp) at the end of each run for easier troubleshooting.