Безопасность цепочки поставок
Что такое Supply Chain Security
Software supply chain — все компоненты, инструменты и процессы, участвующие в создании и доставке ПО. Атака на цепочку поставок — компрометация одного из звеньев для атаки на конечный продукт.
Примеры атак на supply chain
| Атака | Что произошло | Вектор |
|---|---|---|
| SolarWinds (2020) | Malware в обновлении Orion | Compromised build system |
| event-stream (2018) | Вредоносный код в npm-пакете | Social engineering maintainer |
| Codecov (2021) | Модифицированный bash uploader | Compromised CI script |
| Log4Shell (2021) | Уязвимость в популярной библиотеке | Vulnerable dependency |
Поверхность атаки
Source Code → Dependencies → Build System → Artifacts → Distribution → Runtime
↓ ↓ ↓ ↓ ↓ ↓
Tampering Malicious Compromised Modified Fake Vulnerable
by insider packages CI/CD binaries registry configs
Dependency Scanning
Типы сканирования
| Тип | Что проверяет | Инструменты |
|---|---|---|
| SCA (Software Composition Analysis) | Известные уязвимости (CVE) | composer audit, Snyk |
| License compliance | Лицензии зависимостей | FOSSA, license-checker |
| Malware detection | Вредоносный код | Socket.dev, Phylum |
| Outdated dependencies | Устаревшие версии | Dependabot, Renovate |
PHP: автоматическая проверка зависимостей
<?php
declare(strict_types=1);
namespace App\Security;
/**
* Dependency vulnerability checker.
* Wraps `composer audit` and provides structured results.
*/
final readonly class DependencyScanner
{
/**
* Parse the output of `composer audit --format=json`.
*
* @param string $auditJson JSON output from composer audit
* @return SecurityReport
*/
public function parseAuditReport(string $auditJson): SecurityReport
{
$data = json_decode($auditJson, true, 512, JSON_THROW_ON_ERROR);
$advisories = [];
foreach ($data['advisories'] ?? [] as $packageName => $packageAdvisories) {
foreach ($packageAdvisories as $advisory) {
$advisories[] = new SecurityAdvisory(
packageName: $packageName,
advisoryId: $advisory['advisoryId'] ?? 'unknown',
title: $advisory['title'] ?? '',
severity: $advisory['severity'] ?? 'unknown',
affectedVersions: $advisory['affectedVersions'] ?? '',
cve: $advisory['cve'] ?? null,
link: $advisory['link'] ?? null,
);
}
}
return new SecurityReport(
advisories: $advisories,
scannedAt: new \DateTimeImmutable(),
);
}
}
final readonly class SecurityAdvisory
{
public function __construct(
public string $packageName,
public string $advisoryId,
public string $title,
public string $severity,
public string $affectedVersions,
public ?string $cve,
public ?string $link,
) {}
public function isCritical(): bool
{
return in_array(strtolower($this->severity), ['critical', 'high'], true);
}
}
final readonly class SecurityReport
{
/** @param array<SecurityAdvisory> $advisories */
public function __construct(
public array $advisories,
public \DateTimeImmutable $scannedAt,
) {}
public function hasCritical(): bool
{
foreach ($this->advisories as $advisory) {
if ($advisory->isCritical()) {
return true;
}
}
return false;
}
public function countBySeverity(): array
{
$counts = [];
foreach ($this->advisories as $advisory) {
$sev = strtolower($advisory->severity);
$counts[$sev] = ($counts[$sev] ?? 0) + 1;
}
return $counts;
}
}
package security
import (
"encoding/json"
"strings"
"time"
)
// SecurityAdvisory represents a vulnerability advisory.
type SecurityAdvisory struct {
PackageName string `json:"package_name"`
AdvisoryID string `json:"advisory_id"`
Title string `json:"title"`
Severity string `json:"severity"`
AffectedVersions string `json:"affected_versions"`
CVE string `json:"cve,omitempty"`
Link string `json:"link,omitempty"`
}
// IsCritical returns true for critical or high severity.
func (a SecurityAdvisory) IsCritical() bool {
sev := strings.ToLower(a.Severity)
return sev == "critical" || sev == "high"
}
// SecurityReport holds the results of a dependency scan.
type SecurityReport struct {
Advisories []SecurityAdvisory `json:"advisories"`
ScannedAt time.Time `json:"scanned_at"`
}
// HasCritical returns true if any advisory is critical.
func (r SecurityReport) HasCritical() bool {
for _, a := range r.Advisories {
if a.IsCritical() {
return true
}
}
return false
}
// CountBySeverity returns advisory counts grouped by severity.
func (r SecurityReport) CountBySeverity() map[string]int {
counts := make(map[string]int)
for _, a := range r.Advisories {
counts[strings.ToLower(a.Severity)]++
}
return counts
}
// In Go, use `govulncheck ./...` for dependency vulnerability scanning.
// This struct parses its JSON output similarly.
SBOM — машиночитаемый список всех компонентов ПО с версиями, лицензиями и зависимостями. Аналог состава продуктов на упаковке.
Форматы SBOM
| Формат | Описание | Стандарт |
|---|---|---|
| SPDX | Linux Foundation стандарт | ISO/IEC 5962:2021 |
| CycloneDX | OWASP стандарт | Более детальный для security |
| SWID | ISO/IEC 19770-2 | Для лицензирования |
PHP: генерация SBOM
<?php
declare(strict_types=1);
namespace App\Security;
/**
* Generate SBOM from composer.lock.
*/
final readonly class SbomGenerator
{
/**
* Generate CycloneDX-compatible SBOM from composer.lock.
*
* @param string $composerLockPath Path to composer.lock
* @return array SBOM in CycloneDX format
*/
public function generate(string $composerLockPath): array
{
$lock = json_decode(
file_get_contents($composerLockPath),
true,
512,
JSON_THROW_ON_ERROR,
);
$components = [];
foreach ($lock['packages'] ?? [] as $package) {
$components[] = [
'type' => 'library',
'name' => $package['name'],
'version' => $package['version'],
'purl' => sprintf('pkg:composer/%s@%s', $package['name'], $package['version']),
'licenses' => $this->extractLicenses($package),
'hashes' => [
[
'alg' => 'SHA-256',
'content' => $package['dist']['shasum'] ?? '',
],
],
];
}
return [
'bomFormat' => 'CycloneDX',
'specVersion' => '1.5',
'version' => 1,
'metadata' => [
'timestamp' => (new \DateTimeImmutable())->format(\DATE_ATOM),
'tools' => [
['name' => 'sbom-generator', 'version' => '1.0.0'],
],
],
'components' => $components,
];
}
private function extractLicenses(array $package): array
{
$licenses = [];
foreach ($package['license'] ?? [] as $license) {
$licenses[] = ['license' => ['id' => $license]];
}
return $licenses;
}
}
package security
import (
"encoding/json"
"os"
"time"
)
// SBOMComponent represents a software component in the bill of materials.
type SBOMComponent struct {
Type string `json:"type"`
Name string `json:"name"`
Version string `json:"version"`
PURL string `json:"purl"`
}
// GenerateSBOM creates a CycloneDX-compatible SBOM from go.sum.
// In practice, use tools like `cyclonedx-gomod` or `syft`.
func GenerateSBOM(goModPath string) (map[string]any, error) {
data, err := os.ReadFile(goModPath)
if err != nil {
return nil, err
}
// Parse go.mod for dependencies (simplified)
var components []SBOMComponent
// In production, parse go.mod/go.sum properly
_ = data
return map[string]any{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": map[string]any{
"timestamp": time.Now().Format(time.RFC3339),
"tools": []map[string]string{{"name": "go-sbom", "version": "1.0.0"}},
},
"components": components,
}, nil
}
SLSA (Supply-chain Levels for Software Artifacts) — фреймворк Google для защиты цепочки поставок.
Уровни SLSA
| Уровень | Требования | Защита |
|---|---|---|
| SLSA 1 | Документированный build process | Знаем, как построено |
| SLSA 2 | Hosted build service + provenance | Верифицируемый build |
| SLSA 3 | Hardened build platform | Защита от tampering |
| SLSA 4 | Two-person review + hermetic build | Максимальная гарантия |
Provenance (происхождение)
Provenance — метаданные, описывающие как, где и из чего построен артефакт.
<?php
declare(strict_types=1);
namespace App\Security;
/**
* Generate build provenance metadata.
*/
final readonly class ProvenanceGenerator
{
/**
* @return array Build provenance in SLSA format
*/
public function generate(
string $repoUrl,
string $commitSha,
string $builderId,
string $artifactHash,
): array {
return [
'_type' => 'https://in-toto.io/Statement/v0.1',
'predicateType' => 'https://slsa.dev/provenance/v0.2',
'subject' => [
[
'name' => 'app.phar',
'digest' => ['sha256' => $artifactHash],
],
],
'predicate' => [
'builder' => ['id' => $builderId],
'buildType' => 'https://github.com/actions/runner',
'invocation' => [
'configSource' => [
'uri' => $repoUrl,
'digest' => ['sha1' => $commitSha],
],
],
'metadata' => [
'buildStartedOn' => (new \DateTimeImmutable())->format(\DATE_ATOM),
'reproducible' => false,
],
'materials' => [
[
'uri' => $repoUrl,
'digest' => ['sha1' => $commitSha],
],
],
],
];
}
}
package security
import "time"
// GenerateProvenance creates SLSA-compatible build provenance metadata.
func GenerateProvenance(repoURL, commitSHA, builderID, artifactHash string) map[string]any {
return map[string]any{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v0.2",
"subject": []map[string]any{
{
"name": "app",
"digest": map[string]string{"sha256": artifactHash},
},
},
"predicate": map[string]any{
"builder": map[string]string{"id": builderID},
"buildType": "https://github.com/actions/runner",
"invocation": map[string]any{
"configSource": map[string]any{
"uri": repoURL,
"digest": map[string]string{"sha1": commitSHA},
},
},
"metadata": map[string]any{
"buildStartedOn": time.Now().Format(time.RFC3339),
"reproducible": false,
},
"materials": []map[string]any{
{
"uri": repoURL,
"digest": map[string]string{"sha1": commitSHA},
},
},
},
}
}
Sigstore — проект для подписи, верификации и защиты ПО. Упрощает криптографическую подпись артефактов.
| Компонент | Назначение |
|---|---|
| Cosign | Подпись контейнер-образов |
| Rekor | Transparency log (публичный реестр подписей) |
| Fulcio | Выдача короткоживущих сертификатов |
Практики защиты Supply Chain
| Практика | Описание |
|---|---|
| Lock files | Всегда коммитить composer.lock |
| Hash verification | Проверять хеши при установке |
| Private registry | Собственный Composer registry |
| Dependency pinning | Фиксировать точные версии |
| Automated scanning | CI/CD pipeline с composer audit |
| SBOM generation | Генерировать SBOM на каждый релиз |
| Code signing | Подписывать артефакты |
| Reproducible builds | Один и тот же input = один output |
Важно: Supply chain security — это не одноразовое действие. Это непрерывный процесс: сканирование при каждом билде, мониторинг новых уязвимостей, обновление зависимостей.
Итоги
| Концепция | Суть |
|---|---|
| Dependency scanning | composer audit на каждый билд |
| SBOM | Список всех компонентов ПО |
| SLSA | Framework для защиты build pipeline |
| Provenance | Метаданные о происхождении артефакта |
| Sigstore | Подпись и верификация артефактов |
| Lock files | Фиксация точных версий зависимостей |