PHP SBOM Guide: Composer Dependencies and Security
Quick Answer
For most Composer-based PHP applications, generate the SBOM fromcomposer.lock, then validate the file before sharing it with security, compliance, or procurement teams. Use a container or filesystem scanner such as Syft when the deployed PHP app also includes WordPress plugins, themes, system packages, frontend assets, or container image dependencies outside Composer.
A practical PHP SBOM workflow is:
- generate package inventory from Composer or the deployed container
- choose CycloneDX for security automation or SPDX for license-heavy workflows
- validate the result with the SBOM Validator
- store the SBOM with the release artifact or deployment image
Introduction: PHP's Complex Dependency Landscape
PHP's ecosystem, powered by Composer's dependency management model, now spans traditional CMS deployments, modern frameworks, APIs, and containerized workloads. That breadth creates a real inventory problem: a typical Laravel application can pull in 100+ direct and transitive packages, while WordPress and Drupal deployments often mix core code, plugins, themes, and custom modules from different sources.
The challenge is not just generating an SBOM, but generating one that reflects how the application is actually built and deployed. PHP workloads may run on shared hosting, containers, managed platforms, or hybrid stacks with frontend and infrastructure dependencies outsidecomposer.lock. Legacy PHP 5.x and 7.x estates often sit beside newer PHP 8.x services, so teams need a workflow that works across both modern Composer projects and older applications.
From Laravel and Symfony to WordPress and Drupal, PHP applications depend heavily on third-party packages, making dependency visibility essential for security reviews, license tracking, and procurement requests. This guide focuses on practical PHP SBOM generation, when to use CycloneDX vs SPDX, and how to validate the output with the SBOM Validator before wiring it into CI/CD.
Why PHP SBOM Generation Is Critical
Understanding PHP's Unique Security Challenges
PHP powers approximately 78% of websites with known server-side languages, including major platforms like WordPress (43% of all websites), Facebook, and Wikipedia. This massive footprint makes PHP applications attractive targets for attackers, and the language's history of security vulnerabilities adds urgency to supply chain security efforts. Recent supply chain attacks targeting PHP packages have demonstrated that even popular, well-maintained packages can be compromised.
The PHP Ecosystem Landscape
๐ Massive Package Ecosystem- Packagist.org: 380,000+ packages with billions of downloads
- Complex Dependency Trees: Deep transitive dependencies through Composer
- Framework Ecosystems: Laravel (50+ first-party packages), Symfony (60+ components)
- Legacy Systems: WordPress plugins, Drupal modules, custom frameworks
- Supply Chain Attacks: Package takeovers and malicious updates
- Vulnerability Propagation: Transitive dependencies with security flaws
- Outdated Dependencies: Long-lived applications with stale packages
- Dev vs. Production: Different dependency sets for different environments
- Open Source License Tracking: GPL, MIT, Apache licenses in complex combinations
- Commercial License Management: Premium packages and proprietary code
- Regulatory Compliance: GDPR, HIPAA, PCI-DSS requirements for web applications
- Audit Trails: Change tracking for financial and healthcare applications
Business Impact for PHP Organizations
โก Enhanced Security Posture- Rapid Vulnerability Response: Identify affected packages in minutes vs. hours
- Supply Chain Attack Prevention: Monitor for suspicious package changes
- Compliance Automation: Automated license and security compliance checking
- Risk Assessment: Quantify third-party dependency risks
- Dependency Optimization: Identify unused or redundant packages
- Update Planning: Strategic dependency update roadmaps
- License Cost Management: Track commercial license usage
- Technical Debt Reduction: Systematic legacy dependency cleanup
๐ ๏ธ Tool Ecosystem Overview
Selecting the Right SBOM Tool for PHP
Choosing the appropriate SBOM generation tool for PHP depends on your application architecture, deployment model, and compliance requirements. Composer-based applications benefit from tools that understand composer.json and composer.lock files, while legacy applications might require filesystem scanning approaches. Understanding each tool's strengths helps you build a comprehensive SBOM strategy.
PHP-Specific SBOM Tools
| Tool | Approach | Strengths | Best Use Case |
|---|---|---|---|
| CycloneDX PHP Composer | Composer.lock parsing | Native Composer integration | Modern PHP applications |
| Syft | Filesystem scanning | Multi-language support | Containerized PHP apps |
| PHP Security Checker | Security-focused | Vulnerability detection | Security analysis |
| Composer Audit | Built-in security | Native Composer tool | Development workflow |
Cross-Platform Integration
| Tool | PHP Support Quality | Integration Level | Enterprise Features |
|---|---|---|---|
| FOSSA | โญโญโญโญโญ | Deep Composer analysis | Policy management |
| Snyk | โญโญโญโญโญ | Real-time monitoring | Fix automation |
| Mend | โญโญโญโญ | Good package detection | License compliance |
| JFrog Xray | โญโญโญโญ | Artifact analysis | DevOps integration |
๐ฆ Installation and Setup
CycloneDX PHP Composer Plugin
Global Installation (Recommended)# Install globally via Composer
composer global require cyclonedx/cyclonedx-php-composer
# Ensure global Composer bin is in PATH
export PATH="$PATH:$HOME/.composer/vendor/bin"
# Verify the Composer command is available
composer CycloneDX:make-sbom --help# Install as development dependency
composer require --dev cyclonedx/cyclonedx-php-composer
# Use via Composer scripts
composer CycloneDX:make-sbom --help
# Add to composer.json scripts
cat >> composer.json << 'EOF'
{
"scripts": {
"sbom": "composer CycloneDX:make-sbom",
"sbom:json": "composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json",
"sbom:xml": "composer CycloneDX:make-sbom --output-format=XML --output-file=sbom.xml"
}
}
EOFAlternative Tools Setup
Syft for PHP Projects# Install Syft
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
# Test PHP detection
syft . --catalogers php-composer-cataloger# Install security checker
composer global require sensiolabs/security-checker
# Or use the new Symfony CLI (recommended)
wget https://get.symfony.com/cli/installer -O - | bash
sudo mv ~/.symfony/bin/symfony /usr/local/bin/symfony# SPDX tooling is format-oriented rather than Composer-specific
pip install spdx-toolsDevelopment Environment Integration
PhpStorm Integration<!-- .idea/runConfigurations/Generate_SBOM.xml -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Generate SBOM" type="PhpLocalRunConfigurationType" factoryName="PHP Console">
<option name="commandLineParameters" value="CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json" />
<option name="interpreterOptions" value="" />
<option name="pathToPhpExecutable" value="" />
<option name="pathToScript" value="composer" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method v="2" />
</configuration>
</component>// .vscode/tasks.json
{
"version": "2.0.0",
"tasks": [
{
"label": "Generate PHP SBOM",
"type": "shell",
"command": "composer",
"args": ["run-script", "sbom:json"],
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"clear": true
},
"problemMatcher": []
},
{
"label": "Security Audit",
"type": "shell",
"command": "composer",
"args": ["audit"],
"group": "test",
"presentation": {
"echo": true,
"reveal": "always"
}
}
]
}#!/bin/bash
# .git/hooks/pre-commit - Generate SBOM before commits
echo "๐ Generating PHP SBOM..."
# Check if composer.json has changed
if git diff --cached --name-only | grep -q "composer.json\|composer.lock"; then
echo "๐ฆ Composer files changed, generating new SBOM..."
# Generate SBOM
composer run-script sbom:json
# Add SBOM to commit
git add sbom.json
echo "โ
SBOM generated and added to commit"
else
echo "โน๏ธ No Composer changes detected"
fi๐ฏ Basic Usage Patterns
Simple Project SBOM Generation
Basic Laravel Application# Navigate to Laravel project
cd my-laravel-app
# Generate basic SBOM
composer CycloneDX:make-sbom
# Generate with specific format and output
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=laravel-sbom.json
# Generate a production-focused SBOM
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=laravel-production-sbom.json \
--omit=dev# Generate Symfony SBOM
cd symfony-project
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=symfony-sbom.json# Modern WordPress with Composer (using Bedrock or similar)
cd wordpress-site
# Generate SBOM including WordPress core and plugins
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=wordpress-sbom.jsonFramework-Specific Examples
Laravel Project with Multiple Environments// composer.json for Laravel with environment-specific packages
{
"name": "company/laravel-app",
"type": "project",
"require": {
"php": "^8.1",
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7",
"predis/predis": "^2.0"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.0",
"spatie/laravel-ignition": "^2.0"
},
"scripts": {
"sbom:production": "composer CycloneDX:make-sbom --output-format=JSON --output-file=production-sbom.json --omit=dev",
"sbom:development": "composer CycloneDX:make-sbom --output-format=JSON --output-file=development-sbom.json",
"sbom:all": ["@sbom:production", "@sbom:development"],
"security:check": "composer audit",
"security:full": [
"@security:check",
"@sbom:production",
"grype sbom:production-sbom.json"
]
}
}# Generate environment-specific SBOMs
composer run sbom:production
composer run sbom:development
# Run full security analysis
composer run security:full# Generate SBOM for Symfony bundle
cd symfony-bundle
# Bundle-specific SBOM with metadata
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=bundle-sbom.json \
--omit=devAdvanced Composer Scenarios
Monorepo with Multiple PHP Applications#!/bin/bash
# generate-monorepo-sboms.sh - Handle PHP monorepo SBOM generation
MONOREPO_ROOT="$1"
OUTPUT_DIR="${2:-sboms}"
if [ -z "$MONOREPO_ROOT" ]; then
echo "Usage: $0 <monorepo-root> [output-dir]"
exit 1
fi
echo "๐ข PHP Monorepo SBOM Generation"
echo "=============================="
mkdir -p "$OUTPUT_DIR"
# Find all composer.json files
find "$MONOREPO_ROOT" -name "composer.json" -type f | while read composer_file; do
project_dir=$(dirname "$composer_file")
project_name=$(basename "$project_dir")
echo "๐ฆ Processing: $project_name"
cd "$project_dir"
# Skip if no composer.lock (not installed)
if [ ! -f "composer.lock" ]; then
echo " โ ๏ธ No composer.lock found, skipping"
continue
fi
# Generate SBOM
output_file="$OUTPUT_DIR/${project_name}-sbom.json"
if composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file="$output_file"; then
# Validate generated SBOM
component_count=$(jq '.components | length' "$output_file" 2>/dev/null || echo "0")
echo " โ
Generated SBOM with $component_count components"
# Add monorepo context
jq --arg repo_root "$MONOREPO_ROOT" --arg subproject "$project_name" \
'.metadata.properties += [
{"name": "monorepo.root", "value": $repo_root},
{"name": "monorepo.subproject", "value": $subproject}
]' "$output_file" > "${output_file}.tmp" && mv "${output_file}.tmp" "$output_file"
else
echo " โ Failed to generate SBOM"
fi
cd - > /dev/null
done
echo ""
echo "๐ Monorepo SBOM Summary:"
ls -la "$OUTPUT_DIR"/*.json | wc -l | xargs echo "Generated SBOMs:"
find "$OUTPUT_DIR" -name "*.json" -exec jq -r '.components | length' {} \; | awk '{sum+=$1} END {print "Total components: " sum}'// composer.json with private repositories
{
"repositories": [
{
"type": "composer",
"url": "https://repo.packagist.com/company/"
},
{
"type": "vcs",
"url": "git@gitlab.company.com:php/internal-lib.git"
},
{
"type": "path",
"url": "./local-packages/*"
}
],
"require": {
"company/internal-package": "^2.0",
"company/another-lib": "dev-main",
"local/custom-component": "@dev"
}
}# Generate SBOM with private repository context
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=private-sbom.json
# Enhance SBOM with repository information
jq '.components[] |= (
if .purl then
.properties += [{"name": "repository.type", "value": "private"}]
else . end
)' private-sbom.json > enhanced-sbom.json๐๏ธ Framework-Specific Integration
Laravel Deep Integration
Laravel Service Provider for SBOM<?php
// app/Providers/SbomServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Console\Scheduling\Schedule;
class SbomServiceProvider extends ServiceProvider
{
public function register()
{
$this->commands([
\App\Console\Commands\GenerateSbom::class,
\App\Console\Commands\SecurityAudit::class,
]);
}
public function boot()
{
$this->publishes([
__DIR__.'/../../config/sbom.php' => config_path('sbom.php'),
], 'sbom-config');
// Schedule automatic SBOM generation
$this->app->booted(function () {
$schedule = $this->app->make(Schedule::class);
// Generate SBOM weekly
$schedule->command('sbom:generate')
->weekly()
->sundays()
->at('02:00')
->withoutOverlapping();
// Security audit daily
$schedule->command('security:audit')
->daily()
->at('06:00');
});
}
}<?php
// app/Console/Commands/GenerateSbom.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;
use Illuminate\Support\Facades\Storage;
class GenerateSbom extends Command
{
protected $signature = 'sbom:generate
{--format=json : Output format (json|xml)}
{--include-dev : Include development dependencies}
{--upload : Upload to configured storage}';
protected $description = 'Generate SBOM for the Laravel application';
public function handle()
{
$this->info('๐ Generating Laravel SBOM...');
$format = $this->option('format');
$includeDev = $this->option('include-dev');
$upload = $this->option('upload');
// Build command
$command = [
'composer',
'CycloneDX:make-sbom',
'--output-format=' . $format,
'--output-file=storage/app/sbom.' . $format
];
if (!$includeDev) {
$command[] = '--omit=dev';
}
// Execute SBOM generation
$process = new Process($command, base_path());
$process->run();
if (!$process->isSuccessful()) {
$this->error('Failed to generate SBOM: ' . $process->getErrorOutput());
return 1;
}
$this->info('โ
SBOM generated successfully');
// Parse and display summary
$sbomPath = storage_path('app/sbom.' . $format);
$this->displaySbomSummary($sbomPath);
// Upload if requested
if ($upload) {
$this->uploadSbom($sbomPath);
}
return 0;
}
private function getAppVersion(): string
{
// Try to get version from git
$process = new Process(['git', 'describe', '--tags', '--abbrev=0'], base_path());
$process->run();
if ($process->isSuccessful()) {
return trim($process->getOutput());
}
// Fallback to config or composer.json
return config('app.version', '1.0.0');
}
private function displaySbomSummary(string $sbomPath): void
{
if (!file_exists($sbomPath)) {
return;
}
$sbom = json_decode(file_get_contents($sbomPath), true);
$componentCount = count($sbom['components'] ?? []);
$this->info("๐ฆ Components: {$componentCount}");
// License summary
$licenses = [];
foreach ($sbom['components'] ?? [] as $component) {
foreach ($component['licenses'] ?? [] as $license) {
$licenseName = $license['license']['name'] ?? $license['license']['id'] ?? 'Unknown';
$licenses[$licenseName] = ($licenses[$licenseName] ?? 0) + 1;
}
}
if ($licenses) {
$this->info('๐ License distribution:');
arsort($licenses);
foreach (array_slice($licenses, 0, 5, true) as $license => $count) {
$this->line(" โข {$license}: {$count}");
}
}
}
private function uploadSbom(string $sbomPath): void
{
if (!config('sbom.storage.enabled')) {
$this->warn('SBOM upload not configured');
return;
}
$this->info('๐ค Uploading SBOM...');
$disk = Storage::disk(config('sbom.storage.disk', 's3'));
$filename = 'sboms/' . config('app.name') . '/' . date('Y-m-d-H-i-s') . '-sbom.json';
if ($disk->put($filename, file_get_contents($sbomPath))) {
$this->info("โ
SBOM uploaded to: {$filename}");
} else {
$this->error('Failed to upload SBOM');
}
}
}<?php
// config/sbom.php
return [
'enabled' => env('SBOM_ENABLED', true),
'generation' => [
'format' => env('SBOM_FORMAT', 'json'),
'include_dev' => env('SBOM_INCLUDE_DEV', false),
'component_name' => env('APP_NAME', 'Laravel App'),
'component_version' => env('APP_VERSION', '1.0.0'),
],
'storage' => [
'enabled' => env('SBOM_STORAGE_ENABLED', false),
'disk' => env('SBOM_STORAGE_DISK', 's3'),
'path' => env('SBOM_STORAGE_PATH', 'sboms'),
],
'security' => [
'audit_enabled' => env('SBOM_SECURITY_AUDIT', true),
'vulnerability_sources' => [
'composer_audit',
'snyk',
'grype'
],
],
'compliance' => [
'frameworks' => ['SOX', 'PCI-DSS'],
'license_policy' => storage_path('sbom/license-policy.json'),
],
];Symfony Bundle Integration
Symfony Bundle for SBOM Management<?php
// src/Bundle/SbomBundle/SbomBundle.php
namespace App\Bundle\SbomBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class SbomBundle extends Bundle
{
public function build(ContainerBuilder $container): void
{
parent::build($container);
$container->addCompilerPass(new SbomCompilerPass());
}
}<?php
// src/Bundle/SbomBundle/Command/SbomGenerateCommand.php
namespace App\Bundle\SbomBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class SbomGenerateCommand extends Command
{
protected static $defaultName = 'app:sbom:generate';
protected static $defaultDescription = 'Generate SBOM for Symfony application';
private string $projectDir;
public function __construct(string $projectDir)
{
$this->projectDir = $projectDir;
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format', 'json')
->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Output file', 'sbom.json')
->addOption('include-dev', null, InputOption::VALUE_NONE, 'Include dev dependencies')
->addOption('validate', null, InputOption::VALUE_NONE, 'Validate generated SBOM');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$io->title('Symfony SBOM Generator');
$format = $input->getOption('format');
$outputFile = $input->getOption('output');
$includeDev = $input->getOption('include-dev');
$validate = $input->getOption('validate');
// Build generation command
$command = [
'composer',
'CycloneDX:make-sbom',
'--output-format=' . $format,
'--output-file=' . $outputFile
];
if (!$includeDev) {
$command[] = '--omit=dev';
}
$io->section('Generating SBOM...');
try {
$process = new Process($command, $this->projectDir);
$process->mustRun();
$io->success('SBOM generated successfully: ' . $outputFile);
// Display SBOM statistics
$this->displayStats($io, $outputFile);
// Validate if requested
if ($validate) {
$this->validateSbom($io, $outputFile);
}
} catch (ProcessFailedException $exception) {
$io->error('Failed to generate SBOM: ' . $exception->getMessage());
return Command::FAILURE;
}
return Command::SUCCESS;
}
private function displayStats(SymfonyStyle $io, string $sbomFile): void
{
if (!file_exists($sbomFile)) {
return;
}
$sbom = json_decode(file_get_contents($sbomFile), true);
$stats = [
'Components' => count($sbom['components'] ?? []),
'Format' => $sbom['bomFormat'] ?? 'Unknown',
'Spec Version' => $sbom['specVersion'] ?? 'Unknown',
'Generated' => $sbom['metadata']['timestamp'] ?? 'Unknown'
];
$io->definitionList(...array_map(
fn($key, $value) => [$key, $value],
array_keys($stats),
array_values($stats)
));
}
private function validateSbom(SymfonyStyle $io, string $sbomFile): void
{
$io->section('Validating SBOM...');
try {
// Basic JSON validation
$sbom = json_decode(file_get_contents($sbomFile), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \InvalidArgumentException('Invalid JSON: ' . json_last_error_msg());
}
// Required fields validation
$requiredFields = ['bomFormat', 'specVersion', 'serialNumber', 'version'];
foreach ($requiredFields as $field) {
if (!isset($sbom[$field])) {
throw new \InvalidArgumentException("Missing required field: {$field}");
}
}
$io->success('SBOM validation passed');
} catch (\Exception $e) {
$io->error('SBOM validation failed: ' . $e->getMessage());
}
}
}WordPress Modern Setup
WordPress with Composer (Bedrock-style)// composer.json for modern WordPress
{
"name": "company/wordpress-site",
"type": "project",
"repositories": [
{
"type": "composer",
"url": "https://wpackagist.org",
"only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
}
],
"require": {
"php": ">=8.1",
"johnpbloch/wordpress-core": "6.4.2",
"vlucas/phpdotenv": "^5.5",
"oscarotero/env": "^2.1",
"roots/wp-password-bcrypt": "1.1.0",
"wpackagist-plugin/advanced-custom-fields": "^6.2",
"wpackagist-plugin/yoast-seo": "^21.8",
"wpackagist-plugin/wordfence": "^7.11",
"wpackagist-theme/twentytwentyfour": "^1.0"
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7",
"wp-coding-standards/wpcs": "^3.0",
"phpcompatibility/phpcompatibility-wp": "^2.1"
},
"scripts": {
"sbom:generate": "composer CycloneDX:make-sbom --output-format=JSON --output-file=wordpress-sbom.json",
"sbom:production": "composer CycloneDX:make-sbom --output-format=JSON --output-file=production-sbom.json --omit=dev",
"security:check": "composer audit --no-dev",
"wp:update": [
"composer update",
"@sbom:production"
]
}
}# WordPress-specific SBOM generation
composer run sbom:generate
# Enhance WordPress SBOM with plugin information
php -r "
$sbom = json_decode(file_get_contents('wordpress-sbom.json'), true);
// Add WordPress context
$sbom['metadata']['properties'][] = [
'name' => 'wordpress.version',
'value' => '6.4.2'
];
$sbom['metadata']['properties'][] = [
'name' => 'wordpress.type',
'value' => 'composer-managed'
];
// Categorize components
foreach (\$sbom['components'] as &\$component) {
if (strpos(\$component['name'], 'wpackagist-plugin/') === 0) {
\$component['properties'][] = [
'name' => 'wordpress.type',
'value' => 'plugin'
];
} elseif (strpos(\$component['name'], 'wpackagist-theme/') === 0) {
\$component['properties'][] = [
'name' => 'wordpress.type',
'value' => 'theme'
];
} elseif (\$component['name'] === 'johnpbloch/wordpress-core') {
\$component['properties'][] = [
'name' => 'wordpress.type',
'value' => 'core'
];
}
}
file_put_contents('wordpress-enhanced-sbom.json', json_encode(\$sbom, JSON_PRETTY_PRINT));
echo \"WordPress SBOM enhanced with plugin/theme metadata\n\";
"๐ CI/CD Pipeline Integration
GitHub Actions for PHP
Comprehensive PHP SBOM Workflow# .github/workflows/php-sbom.yml
name: PHP SBOM Generation and Security
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday 6 AM
env:
PHP_VERSION: '8.2'
COMPOSER_CACHE_DIR: ~/.composer/cache
jobs:
sbom-generation:
runs-on: ubuntu-latest
outputs:
sbom-file: ${{ steps.generate.outputs.sbom-file }}
component-count: ${{ steps.generate.outputs.component-count }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, dom, filter, gd, json, pcre, hash
tools: composer:v2
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-scripts
- name: Install CycloneDX PHP Composer
run: composer global require cyclonedx/cyclonedx-php-composer
- name: Add Composer global bin to PATH
run: echo "$HOME/.composer/vendor/bin" >> $GITHUB_PATH
- name: Generate SBOM
id: generate
run: |
# Determine project name and version
PROJECT_NAME=$(jq -r '.name // "php-application"' composer.json)
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
else
VERSION="${{ github.sha }}"
fi
echo "Generating SBOM for $PROJECT_NAME version $VERSION"
# Generate production SBOM
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=sbom-production.json \
--omit=dev
# Generate development SBOM
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=sbom-development.json
# Output results
COMPONENT_COUNT=$(jq '.components | length' sbom-production.json)
echo "sbom-file=sbom-production.json" >> $GITHUB_OUTPUT
echo "component-count=$COMPONENT_COUNT" >> $GITHUB_OUTPUT
echo "โ
Generated SBOMs with $COMPONENT_COUNT production components"
- name: Validate SBOMs
run: |
echo "๐ Validating generated SBOMs..."
for sbom in sbom-*.json; do
echo "Validating $sbom"
# JSON validation
if ! jq empty "$sbom"; then
echo "โ Invalid JSON in $sbom"
exit 1
fi
# Basic structure validation
required_fields=("bomFormat" "specVersion" "serialNumber" "version" "metadata" "components")
for field in "${required_fields[@]}"; do
if ! jq -e ".$field" "$sbom" > /dev/null; then
echo "โ Missing required field '$field' in $sbom"
exit 1
fi
done
# Component validation
component_count=$(jq '.components | length' "$sbom")
if [[ "$component_count" -eq 0 ]]; then
echo "โ ๏ธ Warning: No components found in $sbom"
else
echo "โ
$sbom: Valid with $component_count components"
fi
done
- name: Generate SBOM summary
run: |
echo "๐ SBOM Generation Summary" > sbom-summary.md
echo "=========================" >> sbom-summary.md
echo "" >> sbom-summary.md
echo "**Repository**: ${{ github.repository }}" >> sbom-summary.md
echo "**Branch**: ${{ github.ref_name }}" >> sbom-summary.md
echo "**Commit**: ${{ github.sha }}" >> sbom-summary.md
echo "**Generated**: $(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> sbom-summary.md
echo "" >> sbom-summary.md
echo "### Production Dependencies" >> sbom-summary.md
PROD_COUNT=$(jq '.components | length' sbom-production.json)
echo "- **Components**: $PROD_COUNT" >> sbom-summary.md
echo "### Development Dependencies" >> sbom-summary.md
DEV_COUNT=$(jq '.components | length' sbom-development.json)
echo "- **Components**: $DEV_COUNT" >> sbom-summary.md
echo "" >> sbom-summary.md
echo "### License Distribution" >> sbom-summary.md
# Extract licenses and count them
jq -r '.components[]?.licenses[]?.license.name // .components[]?.licenses[]?.license.id // "Unknown"' sbom-production.json | \
sort | uniq -c | sort -nr | head -10 | \
while read count license; do
echo "- **$license**: $count packages" >> sbom-summary.md
done
- name: Upload SBOM artifacts
uses: actions/upload-artifact@v4
with:
name: php-sboms
path: |
sbom-*.json
sbom-summary.md
retention-days: 30
security-analysis:
runs-on: ubuntu-latest
needs: sbom-generation
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ env.PHP_VERSION }}
tools: composer:v2
- name: Install dependencies
run: composer install --prefer-dist --no-interaction
- name: Download SBOM artifacts
uses: actions/download-artifact@v4
with:
name: php-sboms
path: ./
- name: Composer Security Audit
id: composer-audit
run: |
echo "๐ Running Composer security audit..."
# Run composer audit and capture output
if composer audit --format=json > composer-audit.json 2>&1; then
echo "audit-status=passed" >> $GITHUB_OUTPUT
echo "โ
No known security vulnerabilities found"
else
echo "audit-status=failed" >> $GITHUB_OUTPUT
echo "โ ๏ธ Security vulnerabilities detected"
fi
# Parse audit results
if [[ -f composer-audit.json ]]; then
VULN_COUNT=$(jq '.advisories | length' composer-audit.json 2>/dev/null || echo "0")
echo "vulnerability-count=$VULN_COUNT" >> $GITHUB_OUTPUT
else
echo "vulnerability-count=0" >> $GITHUB_OUTPUT
fi
- name: Install Grype for SBOM scanning
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
- name: Scan SBOM for vulnerabilities
id: grype-scan
run: |
echo "๐ Scanning SBOM with Grype..."
# Scan production SBOM
grype sbom:sbom-production.json \
--output json \
--file grype-results.json
grype sbom:sbom-production.json \
--output table \
--file grype-results.txt
# Count vulnerabilities by severity
CRITICAL=$(jq '[.matches[] | select(.vulnerability.severity == "Critical")] | length' grype-results.json)
HIGH=$(jq '[.matches[] | select(.vulnerability.severity == "High")] | length' grype-results.json)
MEDIUM=$(jq '[.matches[] | select(.vulnerability.severity == "Medium")] | length' grype-results.json)
LOW=$(jq '[.matches[] | select(.vulnerability.severity == "Low")] | length' grype-results.json)
echo "critical-count=$CRITICAL" >> $GITHUB_OUTPUT
echo "high-count=$HIGH" >> $GITHUB_OUTPUT
echo "medium-count=$MEDIUM" >> $GITHUB_OUTPUT
echo "low-count=$LOW" >> $GITHUB_OUTPUT
echo "๐ Vulnerability Summary:"
echo " Critical: $CRITICAL"
echo " High: $HIGH"
echo " Medium: $MEDIUM"
echo " Low: $LOW"
# Security gate
if [[ "$CRITICAL" -gt 0 || "$HIGH" -gt 5 ]]; then
echo "security-gate=failed" >> $GITHUB_OUTPUT
echo "โ Security gate failed: Too many high-severity vulnerabilities"
else
echo "security-gate=passed" >> $GITHUB_OUTPUT
echo "โ
Security gate passed"
fi
- name: Generate security report
run: |
cat > security-report.md << EOF
# PHP Security Analysis Report
**Repository**: ${{ github.repository }}
**Scan Date**: $(date -u +'%Y-%m-%d %H:%M:%S UTC')
**Components Analyzed**: ${{ needs.sbom-generation.outputs.component-count }}
## Composer Audit Results
- **Status**: ${{ steps.composer-audit.outputs.audit-status }}
- **Vulnerabilities**: ${{ steps.composer-audit.outputs.vulnerability-count }}
## Grype SBOM Scan Results
- **Critical**: ${{ steps.grype-scan.outputs.critical-count }}
- **High**: ${{ steps.grype-scan.outputs.high-count }}
- **Medium**: ${{ steps.grype-scan.outputs.medium-count }}
- **Low**: ${{ steps.grype-scan.outputs.low-count }}
## Security Gate
**Status**: ${{ steps.grype-scan.outputs.security-gate }}
EOF
if [[ "${{ steps.grype-scan.outputs.security-gate }}" == "failed" ]]; then
cat >> security-report.md << EOF
### โ ๏ธ Action Required
Critical or high-severity vulnerabilities detected. Please review and update affected packages.
EOF
fi
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let comment = `## ๐ PHP SBOM Security Analysis\n\n`;
// Add SBOM summary
if (fs.existsSync('sbom-summary.md')) {
const summary = fs.readFileSync('sbom-summary.md', 'utf8');
comment += summary + '\n\n';
}
// Add security results
if (fs.existsSync('security-report.md')) {
const security = fs.readFileSync('security-report.md', 'utf8');
comment += security;
}
comment += '\n\n๐ Detailed reports available in job artifacts.';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Upload security reports
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
composer-audit.json
grype-results.*
security-report.md
- name: Security gate enforcement
if: steps.grype-scan.outputs.security-gate == 'failed'
run: |
echo "โ Security gate failed!"
echo "Critical: ${{ steps.grype-scan.outputs.critical-count }}"
echo "High: ${{ steps.grype-scan.outputs.high-count }}"
echo "Please review and address security vulnerabilities before merging."
exit 1
deploy-sbom:
runs-on: ubuntu-latest
needs: [sbom-generation, security-analysis]
if: github.ref == 'refs/heads/main' && success()
steps:
- name: Download SBOM artifacts
uses: actions/download-artifact@v4
with:
name: php-sboms
path: ./
- name: Sign SBOM
if: secrets.GPG_PRIVATE_KEY != ''
run: |
echo "${{ secrets.GPG_PRIVATE_KEY }}" | gpg --batch --import
gpg --armor --detach-sign sbom-production.json
echo "โ
SBOM signed"
- name: Upload to SBOM registry
if: secrets.SBOM_REGISTRY_URL != ''
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.SBOM_REGISTRY_TOKEN }}" \
-H "Content-Type: application/json" \
--data-binary @sbom-production.json \
"${{ secrets.SBOM_REGISTRY_URL }}/api/v1/sboms/${{ github.repository }}/production/${{ github.sha }}"
echo "โ
SBOM uploaded to registry"GitLab CI/CD for PHP
GitLab Pipeline Configuration# .gitlab-ci.yml
stages:
- build
- sbom
- security
- deploy
variables:
PHP_VERSION: "8.2"
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
cache:
key: composer-cache
paths:
- .composer-cache/
build:
stage: build
image: php:${PHP_VERSION}-cli
before_script:
- apt-get update && apt-get install -y git unzip
- curl -sS https://getcomposer.org/installer | php
- mv composer.phar /usr/local/bin/composer
script:
- composer install --prefer-dist --no-interaction --optimize-autoloader --no-scripts
artifacts:
paths:
- vendor/
expire_in: 1 hour
generate-sbom:
stage: sbom
image: php:${PHP_VERSION}-cli
dependencies:
- build
before_script:
- apt-get update && apt-get install -y git unzip jq
- curl -sS https://getcomposer.org/installer | php
- mv composer.phar /usr/local/bin/composer
- composer global require cyclonedx/cyclonedx-php-composer
- export PATH="$PATH:$HOME/.composer/vendor/bin"
script:
# Generate production SBOM
- |
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=sbom-production.json \
--omit=dev
# Generate development SBOM
- |
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=sbom-development.json
# Validate SBOMs
- |
for sbom in sbom-*.json; do
echo "Validating $sbom"
jq empty "$sbom" || exit 1
component_count=$(jq '.components | length' "$sbom")
echo "$sbom: $component_count components"
done
# Generate summary
- |
echo "SBOM Summary:" > sbom-summary.txt
echo "=============" >> sbom-summary.txt
echo "Production components: $(jq '.components | length' sbom-production.json)" >> sbom-summary.txt
echo "Development components: $(jq '.components | length' sbom-development.json)" >> sbom-summary.txt
artifacts:
paths:
- sbom-*.json
- sbom-summary.txt
reports:
cyclonedx: sbom-production.json
expire_in: 1 week
security-audit:
stage: security
image: php:${PHP_VERSION}-cli
dependencies:
- build
- generate-sbom
before_script:
- apt-get update && apt-get install -y git unzip curl jq
- curl -sS https://getcomposer.org/installer | php
- mv composer.phar /usr/local/bin/composer
- curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
script:
# Composer audit
- composer audit --format=json > composer-audit.json || true
# Grype SBOM scan
- grype sbom:sbom-production.json --output json --file grype-results.json
- grype sbom:sbom-production.json --output table > grype-results.txt
# Count vulnerabilities
- |
CRITICAL=$(jq '[.matches[] | select(.vulnerability.severity == "Critical")] | length' grype-results.json)
HIGH=$(jq '[.matches[] | select(.vulnerability.severity == "High")] | length' grype-results.json)
echo "Critical vulnerabilities: $CRITICAL"
echo "High vulnerabilities: $HIGH"
if [[ "$CRITICAL" -gt 0 || "$HIGH" -gt 5 ]]; then
echo "Security gate failed: Too many critical/high vulnerabilities"
exit 1
fi
artifacts:
paths:
- composer-audit.json
- grype-results.*
expire_in: 1 week
allow_failure: false
deploy-sbom:
stage: deploy
image: alpine:latest
dependencies:
- generate-sbom
before_script:
- apk add --no-cache curl jq
script:
- |
if [[ -n "$SBOM_REGISTRY_URL" && -n "$SBOM_REGISTRY_TOKEN" ]]; then
echo "Uploading SBOM to registry..."
curl -X POST \
-H "Authorization: Bearer $SBOM_REGISTRY_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @sbom-production.json \
"$SBOM_REGISTRY_URL/api/v1/sboms/$CI_PROJECT_NAME/$CI_COMMIT_SHA"
echo "SBOM uploaded successfully"
else
echo "SBOM registry not configured, skipping upload"
fi
only:
- main๐ง Advanced Security and Compliance
Custom Vulnerability Analysis
Enhanced Security Scanner<?php
// src/Security/PhpSbomSecurityAnalyzer.php
namespace App\Security;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
class PhpSbomSecurityAnalyzer
{
private array $config;
private array $vulnerabilitySources;
public function __construct(array $config = [])
{
$this->config = array_merge([
'composer_audit' => true,
'grype_scan' => true,
'snyk_scan' => false,
'custom_rules' => [],
], $config);
$this->vulnerabilitySources = [];
}
public function analyzeSbom(string $sbomPath): SecurityAnalysisResult
{
$result = new SecurityAnalysisResult();
// Load SBOM
$sbom = $this->loadSbom($sbomPath);
$result->setComponentCount(count($sbom['components'] ?? []));
// Run Composer audit if available
if ($this->config['composer_audit']) {
$this->runComposerAudit($result);
}
// Run Grype scan
if ($this->config['grype_scan']) {
$this->runGrypeScan($sbomPath, $result);
}
// Run Snyk scan if configured
if ($this->config['snyk_scan']) {
$this->runSnykScan($sbomPath, $result);
}
// Apply custom security rules
$this->applyCustomRules($sbom, $result);
// Calculate risk score
$result->calculateRiskScore();
return $result;
}
private function runComposerAudit(SecurityAnalysisResult $result): void
{
try {
$process = new Process(['composer', 'audit', '--format=json']);
$process->run();
if ($process->isSuccessful()) {
$auditData = json_decode($process->getOutput(), true);
foreach ($auditData['advisories'] ?? [] as $advisory) {
$vulnerability = new Vulnerability([
'source' => 'composer',
'package' => $advisory['packageName'],
'severity' => $this->mapComposerSeverity($advisory['severity'] ?? 'medium'),
'cve' => $advisory['cve'] ?? null,
'title' => $advisory['title'] ?? 'Unknown vulnerability',
'description' => $advisory['description'] ?? '',
'affected_versions' => $advisory['affectedVersions'] ?? '',
'fixed_versions' => $advisory['sources'][0]['remoteId'] ?? null,
]);
$result->addVulnerability($vulnerability);
}
}
} catch (ProcessFailedException $e) {
// Composer audit failed - may indicate vulnerabilities
$result->addError('composer_audit_failed', $e->getMessage());
}
}
private function runGrypeScan(string $sbomPath, SecurityAnalysisResult $result): void
{
try {
$process = new Process([
'grype',
"sbom:$sbomPath",
'--output', 'json'
]);
$process->run();
if ($process->isSuccessful()) {
$grypeData = json_decode($process->getOutput(), true);
foreach ($grypeData['matches'] ?? [] as $match) {
$vulnerability = new Vulnerability([
'source' => 'grype',
'package' => $match['artifact']['name'],
'version' => $match['artifact']['version'],
'severity' => strtolower($match['vulnerability']['severity'] ?? 'unknown'),
'cve' => $match['vulnerability']['id'],
'description' => $match['vulnerability']['description'] ?? '',
'fixed_version' => $match['vulnerability']['fix']['versions'][0] ?? null,
'data_source' => $match['vulnerability']['dataSource'] ?? 'unknown',
]);
$result->addVulnerability($vulnerability);
}
}
} catch (ProcessFailedException $e) {
$result->addError('grype_scan_failed', $e->getMessage());
}
}
private function applyCustomRules(array $sbom, SecurityAnalysisResult $result): void
{
foreach ($this->config['custom_rules'] as $rule) {
$this->evaluateCustomRule($rule, $sbom, $result);
}
}
private function evaluateCustomRule(array $rule, array $sbom, SecurityAnalysisResult $result): void
{
$ruleType = $rule['type'];
$condition = $rule['condition'];
switch ($ruleType) {
case 'package_blacklist':
$this->checkPackageBlacklist($condition, $sbom, $result);
break;
case 'license_policy':
$this->checkLicensePolicy($condition, $sbom, $result);
break;
case 'version_policy':
$this->checkVersionPolicy($condition, $sbom, $result);
break;
case 'age_policy':
$this->checkPackageAge($condition, $sbom, $result);
break;
}
}
private function checkPackageBlacklist(array $blacklist, array $sbom, SecurityAnalysisResult $result): void
{
foreach ($sbom['components'] ?? [] as $component) {
$packageName = $component['name'] ?? '';
if (in_array($packageName, $blacklist)) {
$violation = new PolicyViolation([
'type' => 'blacklisted_package',
'severity' => 'high',
'package' => $packageName,
'message' => "Package '$packageName' is blacklisted by security policy",
'recommendation' => 'Remove this package or get security approval',
]);
$result->addPolicyViolation($violation);
}
}
}
private function checkLicensePolicy(array $policy, array $sbom, SecurityAnalysisResult $result): void
{
$forbiddenLicenses = $policy['forbidden'] ?? [];
$requireApprovalLicenses = $policy['require_approval'] ?? [];
foreach ($sbom['components'] ?? [] as $component) {
$licenses = $component['licenses'] ?? [];
$packageName = $component['name'] ?? 'unknown';
foreach ($licenses as $license) {
$licenseName = $license['license']['name'] ?? $license['license']['id'] ?? 'unknown';
if (in_array($licenseName, $forbiddenLicenses)) {
$violation = new PolicyViolation([
'type' => 'forbidden_license',
'severity' => 'critical',
'package' => $packageName,
'license' => $licenseName,
'message' => "Package '$packageName' uses forbidden license '$licenseName'",
'recommendation' => 'Replace with compatible alternative or get legal approval',
]);
$result->addPolicyViolation($violation);
}
if (in_array($licenseName, $requireApprovalLicenses)) {
$violation = new PolicyViolation([
'type' => 'license_requires_approval',
'severity' => 'medium',
'package' => $packageName,
'license' => $licenseName,
'message' => "Package '$packageName' uses license '$licenseName' that requires approval",
'recommendation' => 'Get legal team approval before production use',
]);
$result->addPolicyViolation($violation);
}
}
}
}
private function loadSbom(string $path): array
{
$content = file_get_contents($path);
return json_decode($content, true) ?? [];
}
private function mapComposerSeverity(string $severity): string
{
$mapping = [
'low' => 'low',
'medium' => 'medium',
'high' => 'high',
'critical' => 'critical',
];
return $mapping[strtolower($severity)] ?? 'unknown';
}
}
class SecurityAnalysisResult
{
private int $componentCount = 0;
private array $vulnerabilities = [];
private array $policyViolations = [];
private array $errors = [];
private ?float $riskScore = null;
public function addVulnerability(Vulnerability $vulnerability): void
{
$this->vulnerabilities[] = $vulnerability;
}
public function addPolicyViolation(PolicyViolation $violation): void
{
$this->policyViolations[] = $violation;
}
public function addError(string $type, string $message): void
{
$this->errors[] = ['type' => $type, 'message' => $message];
}
public function calculateRiskScore(): void
{
$score = 0;
// Base score from vulnerabilities
foreach ($this->vulnerabilities as $vuln) {
$score += match($vuln->getSeverity()) {
'critical' => 10,
'high' => 7,
'medium' => 4,
'low' => 1,
default => 0,
};
}
// Policy violations add to risk
foreach ($this->policyViolations as $violation) {
$score += match($violation->getSeverity()) {
'critical' => 8,
'high' => 5,
'medium' => 3,
'low' => 1,
default => 0,
};
}
// Normalize to 0-100 scale
$this->riskScore = min(100, $score);
}
// Getters...
public function getVulnerabilities(): array { return $this->vulnerabilities; }
public function getPolicyViolations(): array { return $this->policyViolations; }
public function getErrors(): array { return $this->errors; }
public function getRiskScore(): ?float { return $this->riskScore; }
public function getComponentCount(): int { return $this->componentCount; }
public function setComponentCount(int $count): void { $this->componentCount = $count; }
}
class Vulnerability
{
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function getSeverity(): string
{
return $this->data['severity'] ?? 'unknown';
}
public function getPackage(): string
{
return $this->data['package'] ?? 'unknown';
}
public function getCve(): ?string
{
return $this->data['cve'] ?? null;
}
public function getDescription(): string
{
return $this->data['description'] ?? '';
}
public function toArray(): array
{
return $this->data;
}
}
class PolicyViolation
{
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function getSeverity(): string
{
return $this->data['severity'] ?? 'unknown';
}
public function getType(): string
{
return $this->data['type'] ?? 'unknown';
}
public function getMessage(): string
{
return $this->data['message'] ?? '';
}
public function toArray(): array
{
return $this->data;
}
}<?php
// config/security.php
return [
'sbom' => [
'security_analysis' => [
'composer_audit' => env('SBOM_COMPOSER_AUDIT', true),
'grype_scan' => env('SBOM_GRYPE_SCAN', true),
'snyk_scan' => env('SBOM_SNYK_SCAN', false),
'custom_rules' => [
[
'type' => 'package_blacklist',
'condition' => [
'vendor/suspicious-package',
'company/deprecated-lib',
],
],
[
'type' => 'license_policy',
'condition' => [
'forbidden' => ['GPL-3.0', 'AGPL-3.0'],
'require_approval' => ['GPL-2.0', 'LGPL-3.0'],
],
],
[
'type' => 'version_policy',
'condition' => [
'min_php_version' => '8.1',
'max_package_age_days' => 365,
],
],
],
],
'risk_thresholds' => [
'low' => 20,
'medium' => 50,
'high' => 80,
'critical' => 100,
],
'reporting' => [
'slack_webhook' => env('SECURITY_SLACK_WEBHOOK'),
'email_recipients' => env('SECURITY_EMAIL_RECIPIENTS'),
'jira_integration' => env('SECURITY_JIRA_ENABLED', false),
],
],
];License Compliance Framework
License Policy Engine<?php
// src/Compliance/LicenseComplianceChecker.php
namespace App\Compliance;
class LicenseComplianceChecker
{
private array $policy;
private array $licenseDb;
public function __construct(array $policy)
{
$this->policy = $policy;
$this->loadLicenseDatabase();
}
public function checkCompliance(string $sbomPath): ComplianceResult
{
$sbom = json_decode(file_get_contents($sbomPath), true);
$result = new ComplianceResult();
foreach ($sbom['components'] ?? [] as $component) {
$this->checkComponentLicenses($component, $result);
}
$result->calculateComplianceScore();
return $result;
}
private function checkComponentLicenses(array $component, ComplianceResult $result): void
{
$packageName = $component['name'] ?? 'unknown';
$licenses = $component['licenses'] ?? [];
if (empty($licenses)) {
$result->addIssue(new ComplianceIssue([
'type' => 'missing_license',
'severity' => 'medium',
'package' => $packageName,
'message' => "Package '$packageName' has no license information",
'recommendation' => 'Investigate license or contact package maintainer',
]));
return;
}
foreach ($licenses as $license) {
$licenseName = $license['license']['name'] ?? $license['license']['id'] ?? 'unknown';
$this->evaluateLicense($licenseName, $packageName, $result);
}
}
private function evaluateLicense(string $licenseName, string $packageName, ComplianceResult $result): void
{
// Check against policy
if (in_array($licenseName, $this->policy['forbidden'])) {
$result->addIssue(new ComplianceIssue([
'type' => 'forbidden_license',
'severity' => 'critical',
'package' => $packageName,
'license' => $licenseName,
'message' => "Package '$packageName' uses forbidden license '$licenseName'",
'recommendation' => 'Remove package or seek legal exception',
]));
}
if (in_array($licenseName, $this->policy['requires_attribution'])) {
$result->addRequirement(new AttributionRequirement([
'package' => $packageName,
'license' => $licenseName,
'attribution_text' => $this->getAttributionText($licenseName),
]));
}
if (in_array($licenseName, $this->policy['copyleft'])) {
$result->addRequirement(new CopyleftRequirement([
'package' => $packageName,
'license' => $licenseName,
'source_code_requirement' => $this->getCopyleftRequirements($licenseName),
]));
}
}
private function loadLicenseDatabase(): void
{
// Load SPDX license database or custom license definitions
$this->licenseDb = [
'MIT' => [
'type' => 'permissive',
'requires_attribution' => true,
'allows_modification' => true,
'allows_distribution' => true,
'allows_private_use' => true,
'allows_commercial_use' => true,
],
'GPL-3.0' => [
'type' => 'copyleft',
'requires_attribution' => true,
'requires_source_code' => true,
'allows_modification' => true,
'allows_distribution' => true,
'allows_private_use' => true,
'allows_commercial_use' => true,
],
// ... more licenses
];
}
private function getAttributionText(string $licenseName): string
{
return match($licenseName) {
'MIT' => 'Copyright notice and license text must be included',
'Apache-2.0' => 'Copyright notice, license text, and NOTICE file must be included',
default => 'Attribution requirements vary - consult license text',
};
}
private function getCopyleftRequirements(string $licenseName): string
{
return match($licenseName) {
'GPL-2.0', 'GPL-3.0' => 'Source code must be made available under same license',
'LGPL-2.1', 'LGPL-3.0' => 'Source code of library must be made available',
'MPL-2.0' => 'Modified files must be made available under MPL-2.0',
default => 'Consult license text for specific requirements',
};
}
}
class ComplianceResult
{
private array $issues = [];
private array $requirements = [];
private ?float $complianceScore = null;
public function addIssue(ComplianceIssue $issue): void
{
$this->issues[] = $issue;
}
public function addRequirement($requirement): void
{
$this->requirements[] = $requirement;
}
public function calculateComplianceScore(): void
{
$totalComponents = 100; // Assume 100 for scoring
$criticalIssues = count(array_filter($this->issues, fn($i) => $i->getSeverity() === 'critical'));
$highIssues = count(array_filter($this->issues, fn($i) => $i->getSeverity() === 'high'));
$mediumIssues = count(array_filter($this->issues, fn($i) => $i->getSeverity() === 'medium'));
// Calculate deductions
$deductions = ($criticalIssues * 20) + ($highIssues * 10) + ($mediumIssues * 5);
$this->complianceScore = max(0, 100 - $deductions);
}
// Getters...
public function getIssues(): array { return $this->issues; }
public function getRequirements(): array { return $this->requirements; }
public function getComplianceScore(): ?float { return $this->complianceScore; }
public function generateReport(): array
{
return [
'compliance_score' => $this->complianceScore,
'total_issues' => count($this->issues),
'critical_issues' => count(array_filter($this->issues, fn($i) => $i->getSeverity() === 'critical')),
'high_issues' => count(array_filter($this->issues, fn($i) => $i->getSeverity() === 'high')),
'medium_issues' => count(array_filter($this->issues, fn($i) => $i->getSeverity() === 'medium')),
'attribution_requirements' => count(array_filter($this->requirements, fn($r) => $r instanceof AttributionRequirement)),
'copyleft_requirements' => count(array_filter($this->requirements, fn($r) => $r instanceof CopyleftRequirement)),
'issues' => array_map(fn($i) => $i->toArray(), $this->issues),
'requirements' => array_map(fn($r) => $r->toArray(), $this->requirements),
];
}
}
class ComplianceIssue
{
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function getSeverity(): string
{
return $this->data['severity'] ?? 'unknown';
}
public function toArray(): array
{
return $this->data;
}
}
class AttributionRequirement
{
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function toArray(): array
{
return $this->data;
}
}
class CopyleftRequirement
{
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function toArray(): array
{
return $this->data;
}
}๐ Performance and Scalability
Large Project Optimization
Parallel SBOM Generation#!/bin/bash
# parallel-php-sbom.sh - High-performance SBOM generation
PROJECTS_DIR="$1"
OUTPUT_DIR="${2:-sboms}"
MAX_PARALLEL="${3:-4}"
if [ -z "$PROJECTS_DIR" ]; then
echo "Usage: $0 <projects-dir> [output-dir] [max-parallel]"
exit 1
fi
echo "๐ Parallel PHP SBOM Generation"
echo "==============================="
echo "Projects directory: $PROJECTS_DIR"
echo "Output directory: $OUTPUT_DIR"
echo "Max parallel jobs: $MAX_PARALLEL"
# Create output directory
mkdir -p "$OUTPUT_DIR"
# Function to generate SBOM for a single project
generate_project_sbom() {
local project_path="$1"
local project_name=$(basename "$project_path")
local output_file="$OUTPUT_DIR/${project_name}-sbom.json"
echo "๐ฆ Processing: $project_name"
# Skip if no composer.json
if [ ! -f "$project_path/composer.json" ]; then
echo " โ ๏ธ No composer.json found, skipping"
return 0
fi
# Skip if no composer.lock
if [ ! -f "$project_path/composer.lock" ]; then
echo " โ ๏ธ No composer.lock found, running composer install..."
cd "$project_path"
composer install --no-scripts --no-plugins --quiet
cd - > /dev/null
fi
# Generate SBOM with timeout
cd "$project_path"
timeout 300s composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file="$output_file" \
> /dev/null 2>&1
local exit_code=$?
cd - > /dev/null
if [ $exit_code -eq 0 ] && [ -f "$output_file" ]; then
# Validate generated SBOM
if jq empty "$output_file" 2>/dev/null; then
local component_count=$(jq '.components | length' "$output_file" 2>/dev/null || echo "0")
echo " โ
$project_name: $component_count components"
else
echo " โ $project_name: Invalid JSON generated"
rm -f "$output_file"
fi
else
echo " โ $project_name: Generation failed or timed out"
fi
}
# Export function for parallel execution
export -f generate_project_sbom
export OUTPUT_DIR
# Find all PHP projects
mapfile -t projects < <(find "$PROJECTS_DIR" -name "composer.json" -type f -exec dirname {} \; | sort -u)
echo "Found ${#projects[@]} PHP projects"
# Process projects in parallel
printf '%s\n' "${projects[@]}" | xargs -n 1 -P "$MAX_PARALLEL" -I {} bash -c 'generate_project_sbom "$@"' _ {}
# Generate consolidated report
echo ""
echo "๐ Generation Summary:"
echo "====================="
total_sboms=$(ls -1 "$OUTPUT_DIR"/*.json 2>/dev/null | wc -l)
echo "Generated SBOMs: $total_sboms"
if [ "$total_sboms" -gt 0 ]; then
total_components=$(find "$OUTPUT_DIR" -name "*.json" -exec jq -r '.components | length' {} \; | awk '{sum+=$1} END {print sum}')
echo "Total components: $total_components"
# License distribution across all projects
echo ""
echo "๐ License Distribution:"
find "$OUTPUT_DIR" -name "*.json" -exec jq -r '.components[]?.licenses[]?.license.name // .components[]?.licenses[]?.license.id // "Unknown"' {} \; | \
sort | uniq -c | sort -nr | head -10 | \
while read count license; do
echo " $license: $count packages"
done
fi
echo ""
echo "๐ Parallel SBOM generation completed!"<?php
// src/Performance/MemoryEfficientSbomProcessor.php
namespace App\Performance;
class MemoryEfficientSbomProcessor
{
private int $batchSize;
private string $tempDir;
public function __construct(int $batchSize = 100)
{
$this->batchSize = $batchSize;
$this->tempDir = sys_get_temp_dir() . '/sbom_processing';
if (!is_dir($this->tempDir)) {
mkdir($this->tempDir, 0755, true);
}
}
public function processLargeSbom(string $sbomPath): ProcessingResult
{
$result = new ProcessingResult();
// Stream process the SBOM file
$handle = fopen($sbomPath, 'r');
if (!$handle) {
throw new \RuntimeException("Cannot open SBOM file: $sbomPath");
}
$result->startTiming();
try {
// Parse SBOM structure
$sbomData = $this->parseJsonStream($handle);
// Process components in batches
$components = $sbomData['components'] ?? [];
$totalComponents = count($components);
$result->setTotalComponents($totalComponents);
for ($offset = 0; $offset < $totalComponents; $offset += $this->batchSize) {
$batch = array_slice($components, $offset, $this->batchSize);
$this->processBatch($batch, $offset, $result);
// Clear memory
unset($batch);
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
}
} finally {
fclose($handle);
$result->endTiming();
}
return $result;
}
private function parseJsonStream($handle): array
{
// For very large files, we might need to use a streaming JSON parser
// For now, we'll use a simple approach
$content = stream_get_contents($handle);
return json_decode($content, true) ?? [];
}
private function processBatch(array $components, int $offset, ProcessingResult $result): void
{
foreach ($components as $index => $component) {
$globalIndex = $offset + $index;
// Process component
$this->processComponent($component, $globalIndex, $result);
// Memory usage check
$memoryUsage = memory_get_usage(true);
$result->updatePeakMemory($memoryUsage);
if ($memoryUsage > $this->getMemoryLimit() * 0.8) {
// Approaching memory limit - force garbage collection
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
// If still high, write to temp file and continue
if (memory_get_usage(true) > $this->getMemoryLimit() * 0.8) {
$this->writeIntermediateResults($result, $globalIndex);
$result->clearAccumulatedData();
}
}
}
}
private function processComponent(array $component, int $index, ProcessingResult $result): void
{
// Extract relevant information
$packageName = $component['name'] ?? 'unknown';
$version = $component['version'] ?? 'unknown';
$licenses = $component['licenses'] ?? [];
// Update statistics
$result->incrementProcessedComponents();
// License tracking
foreach ($licenses as $license) {
$licenseName = $license['license']['name'] ?? $license['license']['id'] ?? 'unknown';
$result->addLicense($licenseName);
}
// Vulnerability context if available
if (isset($component['vulnerabilities'])) {
$result->addVulnerableComponent($packageName, count($component['vulnerabilities']));
}
// Package source analysis
$purl = $component['purl'] ?? '';
if (str_contains($purl, 'composer')) {
$result->addComposerPackage();
}
}
private function getMemoryLimit(): int
{
$limit = ini_get('memory_limit');
if ($limit === '-1') {
return PHP_INT_MAX;
}
$value = (int) $limit;
$unit = strtoupper(substr($limit, -1));
return match($unit) {
'G' => $value * 1024 * 1024 * 1024,
'M' => $value * 1024 * 1024,
'K' => $value * 1024,
default => $value,
};
}
private function writeIntermediateResults(ProcessingResult $result, int $currentIndex): void
{
$filename = $this->tempDir . "/intermediate_results_{$currentIndex}.json";
file_put_contents($filename, json_encode($result->getSerializableData()));
$result->addIntermediateFile($filename);
}
public function __destruct()
{
// Clean up temp files
if (is_dir($this->tempDir)) {
$files = glob($this->tempDir . '/*');
foreach ($files as $file) {
unlink($file);
}
rmdir($this->tempDir);
}
}
}
class ProcessingResult
{
private int $totalComponents = 0;
private int $processedComponents = 0;
private array $licenseDistribution = [];
private array $vulnerableComponents = [];
private int $composerPackages = 0;
private int $peakMemoryUsage = 0;
private float $startTime = 0;
private float $endTime = 0;
private array $intermediateFiles = [];
public function startTiming(): void
{
$this->startTime = microtime(true);
}
public function endTiming(): void
{
$this->endTime = microtime(true);
}
public function getProcessingTime(): float
{
return $this->endTime - $this->startTime;
}
public function setTotalComponents(int $total): void
{
$this->totalComponents = $total;
}
public function incrementProcessedComponents(): void
{
$this->processedComponents++;
}
public function addLicense(string $license): void
{
$this->licenseDistribution[$license] = ($this->licenseDistribution[$license] ?? 0) + 1;
}
public function addVulnerableComponent(string $package, int $vulnCount): void
{
$this->vulnerableComponents[$package] = $vulnCount;
}
public function addComposerPackage(): void
{
$this->composerPackages++;
}
public function updatePeakMemory(int $usage): void
{
$this->peakMemoryUsage = max($this->peakMemoryUsage, $usage);
}
public function addIntermediateFile(string $filename): void
{
$this->intermediateFiles[] = $filename;
}
public function clearAccumulatedData(): void
{
// Keep only essential counters, clear large arrays
$this->licenseDistribution = [];
$this->vulnerableComponents = [];
}
public function getSerializableData(): array
{
return [
'processed_components' => $this->processedComponents,
'license_distribution' => $this->licenseDistribution,
'vulnerable_components' => $this->vulnerableComponents,
'composer_packages' => $this->composerPackages,
];
}
public function generateReport(): array
{
// Merge data from intermediate files if any
foreach ($this->intermediateFiles as $file) {
if (file_exists($file)) {
$data = json_decode(file_get_contents($file), true);
$this->mergeIntermediateData($data);
}
}
return [
'summary' => [
'total_components' => $this->totalComponents,
'processed_components' => $this->processedComponents,
'composer_packages' => $this->composerPackages,
'unique_licenses' => count($this->licenseDistribution),
'vulnerable_components' => count($this->vulnerableComponents),
'processing_time_seconds' => round($this->getProcessingTime(), 2),
'peak_memory_usage_mb' => round($this->peakMemoryUsage / (1024 * 1024), 2),
],
'license_distribution' => $this->licenseDistribution,
'vulnerable_components' => $this->vulnerableComponents,
];
}
private function mergeIntermediateData(array $data): void
{
// Merge license distributions
foreach ($data['license_distribution'] ?? [] as $license => $count) {
$this->licenseDistribution[$license] = ($this->licenseDistribution[$license] ?? 0) + $count;
}
// Merge vulnerable components
foreach ($data['vulnerable_components'] ?? [] as $package => $vulnCount) {
$this->vulnerableComponents[$package] = $vulnCount;
}
$this->composerPackages += $data['composer_packages'] ?? 0;
}
}๐ฎ Future Trends and Advanced Topics
PHP 8.3+ Modern Features
Attribute-Based SBOM Metadata<?php
// Modern PHP with attributes for SBOM metadata
#[Package(
name: "company/advanced-lib",
version: "2.1.0",
type: "library",
security: [
"cve-scanning" => true,
"license-check" => true,
"supply-chain-verification" => true
]
)]
class AdvancedLibrary
{
#[Dependency(
package: "guzzlehttp/guzzle",
version: "^7.5",
scope: "production",
security_critical: true
)]
private HttpClient $httpClient;
#[Dependency(
package: "monolog/monolog",
version: "^3.0",
scope: "production",
pii_handling: true
)]
private Logger $logger;
}
#[\Attribute]
class Package
{
public function __construct(
public string $name,
public string $version,
public string $type = 'library',
public array $security = []
) {}
}
#[\Attribute]
class Dependency
{
public function __construct(
public string $package,
public string $version,
public string $scope = 'production',
public bool $security_critical = false,
public bool $pii_handling = false
) {}
}Cloud-Native PHP Integration
Kubernetes SBOM Sidecar# k8s-php-sbom-sidecar.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: php-app-with-sbom
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: php-app
template:
metadata:
labels:
app: php-app
annotations:
sbom.security/enabled: "true"
sbom.security/format: "cyclonedx"
spec:
containers:
- name: php-app
image: php-app:latest
ports:
- containerPort: 8080
volumeMounts:
- name: sbom-data
mountPath: /var/sbom
# SBOM Generator Sidecar
- name: sbom-generator
image: composer:2
command:
- /bin/sh
- -c
- |
composer global require --no-interaction cyclonedx/cyclonedx-php-composer
while true; do
echo "Generating SBOM for PHP application..."
# Extract app files
cp -r /app/vendor /tmp/analysis/ 2>/dev/null || true
cp /app/composer.* /tmp/analysis/ 2>/dev/null || true
# Generate SBOM
cd /tmp/analysis
composer CycloneDX:make-sbom \
--output-format=JSON \
--output-file=/var/sbom/current-sbom.json
# Upload to SBOM service
curl -X POST \
-H "Authorization: Bearer $SBOM_API_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @/var/sbom/current-sbom.json \
"$SBOM_API_ENDPOINT/upload"
sleep 3600 # Generate hourly
done
env:
- name: SBOM_API_TOKEN
valueFrom:
secretKeyRef:
name: sbom-api-credentials
key: token
- name: SBOM_API_ENDPOINT
value: "https://sbom-service.company.com/api/v1"
volumeMounts:
- name: sbom-data
mountPath: /var/sbom
- name: app-source
mountPath: /app
readOnly: true
volumes:
- name: sbom-data
emptyDir: {}
- name: app-source
configMap:
name: php-app-sourceAI-Enhanced SBOM Analysis
Machine Learning for PHP Package Risk Assessment<?php
// src/AI/PhpPackageRiskAnalyzer.php
namespace App\AI;
class PhpPackageRiskAnalyzer
{
private MLModel $riskModel;
private array $features;
public function __construct()
{
$this->loadRiskModel();
$this->initializeFeatures();
}
public function analyzePackageRisk(array $sbom): RiskAnalysisResult
{
$result = new RiskAnalysisResult();
foreach ($sbom['components'] ?? [] as $component) {
$risk = $this->calculatePackageRisk($component);
$result->addPackageRisk($component['name'], $risk);
}
return $result;
}
private function calculatePackageRisk(array $component): PackageRisk
{
$features = $this->extractFeatures($component);
$riskScore = $this->riskModel->predict($features);
return new PackageRisk([
'package' => $component['name'],
'version' => $component['version'] ?? 'unknown',
'risk_score' => $riskScore,
'risk_factors' => $this->identifyRiskFactors($features),
'recommendations' => $this->generateRecommendations($component, $riskScore),
]);
}
private function extractFeatures(array $component): array
{
return [
// Package metadata features
'package_age_days' => $this->getPackageAge($component),
'version_count' => $this->getVersionCount($component['name']),
'download_count' => $this->getDownloadCount($component['name']),
'github_stars' => $this->getGitHubStars($component),
'maintainer_count' => $this->getMaintainerCount($component['name']),
'last_update_days' => $this->getLastUpdateDays($component),
// Security features
'has_security_policy' => $this->hasSecurityPolicy($component),
'vulnerability_count' => $this->getHistoricalVulnerabilities($component['name']),
'has_code_signing' => $this->hasCodeSigning($component),
// Code quality features
'has_tests' => $this->hasTests($component),
'code_coverage' => $this->getCodeCoverage($component),
'has_ci' => $this->hasContinuousIntegration($component),
// Dependency features
'dependency_count' => $this->getDependencyCount($component),
'circular_dependencies' => $this->hasCircularDependencies($component),
'deprecated_dependencies' => $this->hasDeprecatedDependencies($component),
// License and legal
'license_risk_score' => $this->getLicenseRiskScore($component),
'license_compatibility' => $this->getLicenseCompatibility($component),
];
}
private function identifyRiskFactors(array $features): array
{
$factors = [];
if ($features['package_age_days'] < 30) {
$factors[] = 'Very new package (< 30 days)';
}
if ($features['maintainer_count'] < 2) {
$factors[] = 'Single maintainer dependency';
}
if ($features['last_update_days'] > 365) {
$factors[] = 'Not updated in over a year';
}
if ($features['vulnerability_count'] > 5) {
$factors[] = 'History of multiple vulnerabilities';
}
if (!$features['has_tests']) {
$factors[] = 'No test coverage detected';
}
if ($features['dependency_count'] > 50) {
$factors[] = 'High number of dependencies';
}
return $factors;
}
private function generateRecommendations(array $component, float $riskScore): array
{
$recommendations = [];
if ($riskScore > 0.8) {
$recommendations[] = 'Consider finding alternative package';
$recommendations[] = 'Implement additional monitoring';
$recommendations[] = 'Conduct security review';
} elseif ($riskScore > 0.6) {
$recommendations[] = 'Monitor for updates regularly';
$recommendations[] = 'Review security advisories';
} elseif ($riskScore > 0.4) {
$recommendations[] = 'Standard monitoring sufficient';
}
return $recommendations;
}
// Feature extraction methods (implementations would query various APIs)
private function getPackageAge(array $component): int
{
// Query Packagist API for first release date
return 365; // Placeholder
}
private function getVersionCount(string $packageName): int
{
// Query Packagist API for version history
return 10; // Placeholder
}
private function getDownloadCount(string $packageName): int
{
// Query Packagist API for download statistics
return 1000000; // Placeholder
}
// ... other feature extraction methods
private function loadRiskModel(): void
{
// Load pre-trained ML model for risk assessment
// This could be a TensorFlow model, scikit-learn model, etc.
$this->riskModel = new MockMLModel();
}
private function initializeFeatures(): void
{
$this->features = [
'package_metadata' => ['age', 'popularity', 'maintenance'],
'security_indicators' => ['vulnerabilities', 'policies', 'signing'],
'code_quality' => ['tests', 'coverage', 'ci'],
'dependencies' => ['count', 'freshness', 'circular'],
'licensing' => ['compatibility', 'risk'],
];
}
}
class MockMLModel
{
public function predict(array $features): float
{
// Mock implementation - in reality this would use a trained model
$riskScore = 0.0;
// Simple heuristic-based scoring
if ($features['package_age_days'] < 30) $riskScore += 0.2;
if ($features['maintainer_count'] < 2) $riskScore += 0.3;
if ($features['vulnerability_count'] > 3) $riskScore += 0.4;
if (!$features['has_tests']) $riskScore += 0.1;
return min(1.0, $riskScore);
}
}
class RiskAnalysisResult
{
private array $packageRisks = [];
public function addPackageRisk(string $packageName, PackageRisk $risk): void
{
$this->packageRisks[$packageName] = $risk;
}
public function getHighRiskPackages(float $threshold = 0.7): array
{
return array_filter($this->packageRisks, fn($risk) => $risk->getRiskScore() >= $threshold);
}
public function generateReport(): array
{
$highRisk = $this->getHighRiskPackages();
$mediumRisk = $this->getHighRiskPackages(0.4);
return [
'summary' => [
'total_packages' => count($this->packageRisks),
'high_risk_packages' => count($highRisk),
'medium_risk_packages' => count($mediumRisk) - count($highRisk),
'average_risk_score' => $this->getAverageRiskScore(),
],
'high_risk_packages' => array_map(fn($risk) => $risk->toArray(), $highRisk),
'recommendations' => $this->getGlobalRecommendations(),
];
}
private function getAverageRiskScore(): float
{
if (empty($this->packageRisks)) {
return 0.0;
}
$total = array_sum(array_map(fn($risk) => $risk->getRiskScore(), $this->packageRisks));
return $total / count($this->packageRisks);
}
private function getGlobalRecommendations(): array
{
$highRiskCount = count($this->getHighRiskPackages());
if ($highRiskCount > 10) {
return [
'Consider comprehensive dependency audit',
'Implement automated security monitoring',
'Review package selection policies',
];
} elseif ($highRiskCount > 5) {
return [
'Monitor high-risk packages closely',
'Consider alternatives for highest-risk packages',
];
}
return ['Continue regular monitoring'];
}
}
class PackageRisk
{
private array $data;
public function __construct(array $data)
{
$this->data = $data;
}
public function getRiskScore(): float
{
return $this->data['risk_score'] ?? 0.0;
}
public function toArray(): array
{
return $this->data;
}
}๐ Best Practices and Resources
Development Team Training
PHP SBOM Workshop Curriculum# PHP SBOM Workshop: From Basics to Advanced
## Day 1: Foundations
### Morning Session
- Understanding Software Bills of Materials
- PHP ecosystem security challenges
- Composer dependency management review
- Introduction to CycloneDX format
### Afternoon Session
- Hands-on: First SBOM generation
- SBOM validation and quality checking
- Integration with development workflow
- Common issues and troubleshooting
## Day 2: Advanced Implementation
### Morning Session
- Framework-specific implementations (Laravel, Symfony)
- CI/CD pipeline integration
- Security analysis and vulnerability scanning
- License compliance automation
### Afternoon Session
- Enterprise deployment strategies
- Performance optimization for large projects
- Custom tooling and automation
- Future trends and best practices
## Resources Provided
- Sample projects for each major PHP framework
- CI/CD pipeline templates
- Security policy configurations
- Enterprise deployment guidesCommunity Resources
Essential PHP SBOM Resources- PHP Security Advisories Database
- Composer Security Audit Documentation
- CycloneDX PHP Composer Plugin
- Packagist Security Advisory API
Industry Implementation Examples
Success Stories- E-commerce Platform: Reduced security incident response time by 75%
- Financial Services: Achieved SOX compliance with automated SBOM generation
- Healthcare Application: Met HIPAA requirements through comprehensive dependency tracking
- Government Contractor: Satisfied federal SBOM requirements with CycloneDX integration
Conclusion
PHP SBOM generation has evolved from a security best practice to a business necessity. With Composer's rich dependency ecosystem and the powerful CycloneDX PHP tools, PHP developers have world-class capabilities for creating comprehensive, accurate Software Bills of Materials.
Key Success Factors:- Start with Standards: Use CycloneDX format for maximum compatibility
- Integrate Early: Build SBOM generation into development workflows from day one
- Automate Everything: Use CI/CD pipelines for consistent, reliable SBOM generation
- Monitor Continuously: Combine SBOM generation with security and license monitoring
- Scale Strategically: Use parallel processing and memory-efficient techniques for enterprise applications
The future of PHP development is transparent, secure, and compliantโand it starts with comprehensive Software Bills of Materials that protect your applications and your business. ๐