Hard📖Теория5 min

Безопасность цепочки поставок

Dependency scanning, SBOM, SLSA framework, Sigstore и защита software supply chain

Безопасность цепочки поставок

Что такое 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: автоматическая проверка зависимостей

PHPGo
<?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 (Software Bill of Materials)

SBOM — машиночитаемый список всех компонентов ПО с версиями, лицензиями и зависимостями. Аналог состава продуктов на упаковке.

Форматы SBOM

Формат Описание Стандарт
SPDX Linux Foundation стандарт ISO/IEC 5962:2021
CycloneDX OWASP стандарт Более детальный для security
SWID ISO/IEC 19770-2 Для лицензирования

PHP: генерация SBOM

PHPGo
<?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 Framework

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 — метаданные, описывающие как, где и из чего построен артефакт.

PHPGo
<?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

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 Фиксация точных версий зависимостей