🔒 Building a Security Audit Pipeline
In 15 minutes, combine three security data sources — local AuditWithJSON, remote GetSecurityAdvisories, and ValidateStructured — to produce a JSON report ready for database storage. Requires local PHP + Composer.
Why a Pipeline?
A single source isn't enough: local audits only check installed versions, remote advisories show the latest ecosystem vulnerabilities, and validate catches "composer.json itself is malformed". Three layers combined with a unified report makes a production-ready security pipeline.
Step 1: Local Audit
comp.AuditWithJSON() executes composer audit --format=json and returns *composer.AuditResult. Found is the total vulnerability count, Advisories is the vulnerability list, each containing Package, Title, Severity, Link, CVE, etc.
Step 2: Fetch Remote Advisories
client.GetSecurityAdvisories() returns *domain.AdvisoriesResponse, where Advisories is map[packageName][]*Advisory. Use GetSecurityAdvisoriesForPackages(names) when only concerned about specific packages.
Step 3: Validate composer.json
comp.ValidateStructured() returns *composer.ValidateResult, containing Valid boolean, Errors and Warnings slices.
Complete Runnable Example
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"time"
"github.com/scagogogo/composer-skills/pkg/client"
"github.com/scagogogo/composer-skills/pkg/composer"
)
type Report struct {
GeneratedAt string `json:"generated_at"`
LocalVulns int `json:"local_vulns"`
LocalAdvisories []composer.Vulnerability `json:"local_advisories,omitempty"`
RemotePackages int `json:"remote_packages_with_advisories"`
ComposerJSON *composer.ValidateResult `json:"composer_json,omitempty"`
}
func main() {
comp, err := composer.New(composer.DefaultOptions())
if err != nil {
log.Fatal(err)
}
comp.SetWorkingDir("/tmp/my-php-project")
report := Report{GeneratedAt: time.Now().Format(time.RFC3339)}
// 1️⃣ Local audit
if audit, err := comp.AuditWithJSON(); err == nil {
report.LocalVulns = audit.Found
report.LocalAdvisories = audit.Advisories
for _, v := range audit.Advisories {
fmt.Printf("⚠️ %s: %s [%s] %s\n", v.Package, v.Title, v.Severity, v.Link)
}
}
// 2️⃣ Remote advisories (only check direct dependencies)
deps := comp.GetDirectDependencyNames()
c := client.NewComposerClient(30 * time.Second)
if adv, err := c.GetSecurityAdvisoriesForPackages(deps); err == nil {
report.RemotePackages = len(adv.Advisories)
for pkg, list := range adv.Advisories {
fmt.Printf("🌐 %s: %d remote advisories\n", pkg, len(list))
}
}
// 3️⃣ Validate composer.json
if vr, err := comp.ValidateStructured(); err == nil {
report.ComposerJSON = vr
if !vr.Valid {
fmt.Printf("❌ composer.json invalid: %v\n", vr.Errors)
}
}
// 4️⃣ Write report to disk
b, _ := json.MarshalIndent(report, "", " ")
if err := os.WriteFile("security-report.json", b, 0644); err != nil {
log.Fatal(err)
}
fmt.Printf("\n✅ Report written to security-report.json (local vulns: %d, remote affected packages: %d)\n",
report.LocalVulns, report.RemotePackages)
}Expected Output
⚠️ guzzlehttp/psr7: Objective PHPSR7 vulnerability [high] https://...
🌐 monolog/monolog: 1 remote advisory
❌ composer.json invalid: [The key "require.monolog/monolog" is invalid]
✅ Report written to security-report.json (local vulns: 1, remote affected packages: 1)Advanced: Incremental Advisories & High-Severity Filtering
GetSecurityAdvisoriesSince(t)only returns advisories updated aftert, suitable for scheduled polling.comp.GetHighSeverityVulnerabilities()filtershigh/criticalvulnerabilities after local audit, used to block the pipeline.
// Pipeline blocking: fail if high-severity vulnerabilities found
high, _ := comp.GetHighSeverityVulnerabilities()
if len(high) > 0 {
log.Fatalf("Found %d high-severity vulnerabilities, pipeline failed", len(high))
}Meaning of Structured Return Values
AuditResult and ValidateResult are pure Go structs that can be directly json.Marshal'd into a database or fed to an alerting system, no regex parsing of CLI text needed.
Audit Exit Codes
When composer audit finds vulnerabilities, it exits with a non-zero code, but the SDK converts this case into *AuditResult (Found > 0) instead of throwing an error. Check result.Found for vulnerabilities, not err != nil.
Next Steps
- Put this pipeline into GitHub Actions: 🤖 Auto-Detect & Install in CI/CD.
- Aggregate audit results into a dashboard: 📈 Dependency Dashboard.