HardТеория12 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

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: self::normalizeSeverity($advisory['severity'] ?? null),
                    affectedVersions: $advisory['affectedVersions'] ?? '',
                    cve: $advisory['cve'] ?? null,
                    link: $advisory['link'] ?? null,
                );
            }
        }

        return new SecurityReport(
            advisories: $advisories,
            scannedAt: new \DateTimeImmutable(),
        );
    }

    /**
     * An unknown severity is treated as critical, never ignored: a missing
     * field must fail the build rather than silently pass the gate.
     */
    private static function normalizeSeverity(?string $severity): string
    {
        return ($severity !== null && $severity !== '') ? $severity : 'critical';
    }
}

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;
    }
}
## SBOM (Software Bill of Materials)

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

Форматы SBOM

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

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

<?php

declare(strict_types=1);

namespace App\Security;

/**
 * SLSA in-toto attestation. Emitted by the build pipeline, then signed
 * with cosign and published to a transparency log.
 */
final readonly class ProvenanceGenerator
{
    private const string STATEMENT_TYPE = 'https://in-toto.io/Statement/v0.1';
    private const string PREDICATE_TYPE = 'https://slsa.dev/provenance/v0.2';
    private const string BUILD_TYPE = 'https://github.com/actions/runner';

    /**
     * @return array Build provenance in SLSA format
     */
    public function generate(
        string $repoUrl,
        string $commitSha,
        string $builderId,
        string $artifactName,
        string $artifactHash,
    ): array {
        return [
            '_type' => self::STATEMENT_TYPE,
            'predicateType' => self::PREDICATE_TYPE,
            'subject' => [
                [
                    'name' => $artifactName,
                    'digest' => ['sha256' => $artifactHash],
                ],
            ],
            'predicate' => [
                'builder' => ['id' => $builderId],
                'buildType' => self::BUILD_TYPE,
                'invocation' => [
                    'configSource' => [
                        'uri' => $repoUrl,
                        'digest' => ['sha1' => $commitSha],
                    ],
                ],
                'metadata' => [
                    'buildStartedOn' => (new \DateTimeImmutable())->format(\DATE_ATOM),
                    // Set to true only once the build is genuinely hermetic.
                    'reproducible' => false,
                ],
                'materials' => [
                    [
                        'uri' => $repoUrl,
                        'digest' => ['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 Фиксация точных версий зависимостей