Skip to content

🔒 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

go
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 after t, suitable for scheduled polling.
  • comp.GetHighSeverityVulnerabilities() filters high/critical vulnerabilities after local audit, used to block the pipeline.
go
// 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

Released under the MIT License