Software supply chain — все компоненты, инструменты и процессы, участвующие в создании и доставке ПО. Атака на цепочку поставок — компрометация одного из звеньев для атаки на конечный продукт.
package security
import (
"fmt"
"os"
"time"
"golang.org/x/mod/modfile"
)
// 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.mod.
// In practice prefer a dedicated tool: `cyclonedx-gomod` or `syft`.
func GenerateSBOM(goModPath string) (map[string]any, error) {
data, err := os.ReadFile(goModPath)
if err != nil {
return nil, fmt.Errorf("read go.mod: %w", err)
}
// A partially parsed go.mod must be an error, not a short component list:
// an SBOM that silently omits dependencies hides them from every scanner
// that consumes it.
f, err := modfile.Parse(goModPath, data, nil)
if err != nil {
return nil, fmt.Errorf("parse go.mod: %w", err)
}
components := make([]SBOMComponent, 0, len(f.Require))
for _, req := range f.Require {
components = append(components, SBOMComponent{
Type: "library",
Name: req.Mod.Path,
Version: req.Mod.Version,
PURL: fmt.Sprintf("pkg:golang/%s@%s", req.Mod.Path, req.Mod.Version),
})
}
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
}
using System.Text.Json;
using System.Text.Json.Serialization;
namespace App.Security;
public sealed record SbomComponent(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("version")] string Version,
[property: JsonPropertyName("purl")] string Purl,
[property: JsonPropertyName("licenses")] IReadOnlyList<object> Licenses);
// Reads project.assets.json, produced by `dotnet restore`, which lists the
// fully resolved dependency graph. In practice prefer a dedicated tool:
// `dotnet CycloneDX <project>`.
public static class SbomGenerator
{
public static IReadOnlyDictionary<string, object> Generate(string assetsJsonPath)
{
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(assetsJsonPath));
var components = new List<SbomComponent>();
if (document.RootElement.TryGetProperty("libraries", out JsonElement libraries))
{
foreach (JsonProperty library in libraries.EnumerateObject())
{
// Keys have the form "Package/1.2.3".
string[] parts = library.Name.Split('/', 2);
if (parts.Length != 2)
{
continue;
}
components.Add(new SbomComponent(
Type: "library",
Name: parts[0],
Version: parts[1],
Purl: $"pkg:nuget/{parts[0]}@{parts[1]}",
Licenses: []));
}
}
return new Dictionary<string, object>
{
["bomFormat"] = "CycloneDX",
["specVersion"] = "1.5",
["version"] = 1,
["metadata"] = new Dictionary<string, object>
{
["timestamp"] = DateTimeOffset.UtcNow.ToString("O"),
["tools"] = new[]
{
new Dictionary<string, string>
{
["name"] = "sbom-generator",
["version"] = "1.0.0",
},
},
},
["components"] = components,
};
}
}
import tomllib
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
def generate_sbom(lock_path: Path) -> dict[str, Any]:
"""Generate a CycloneDX SBOM from a uv.lock / poetry.lock file.
In practice prefer a dedicated tool: `cyclonedx-py environment`.
"""
lock = tomllib.loads(lock_path.read_text(encoding="utf-8"))
components = [
{
"type": "library",
"name": package["name"],
"version": package["version"],
"purl": f"pkg:pypi/{package['name']}@{package['version']}",
"licenses": [
{"license": {"id": license_id}}
for license_id in package.get("license", [])
],
"hashes": [
{"alg": "SHA-256", "content": wheel["hash"].removeprefix("sha256:")}
for wheel in package.get("wheels", [])
if wheel.get("hash", "").startswith("sha256:")
],
}
for package in lock.get("package", [])
]
return {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": {
"timestamp": datetime.now(timezone.utc).isoformat(),
"tools": [{"name": "sbom-generator", "version": "1.0.0"}],
},
"components": components,
}
## 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],
],
],
],
];
}
}
package security
import "time"
const (
statementType = "https://in-toto.io/Statement/v0.1"
predicateType = "https://slsa.dev/provenance/v0.2"
buildType = "https://github.com/actions/runner"
)
// GenerateProvenance creates a SLSA in-toto attestation. Emitted by the build
// pipeline, then signed with cosign and published to a transparency log.
func GenerateProvenance(repoURL, commitSHA, builderID, artifactName, artifactHash string) map[string]any {
sourceRef := map[string]any{
"uri": repoURL,
"digest": map[string]string{"sha1": commitSHA},
}
return map[string]any{
"_type": statementType,
"predicateType": predicateType,
"subject": []map[string]any{
{
"name": artifactName,
"digest": map[string]string{"sha256": artifactHash},
},
},
"predicate": map[string]any{
"builder": map[string]string{"id": builderID},
"buildType": buildType,
"invocation": map[string]any{
"configSource": sourceRef,
},
"metadata": map[string]any{
"buildStartedOn": time.Now().Format(time.RFC3339),
// Set to true only once the build is genuinely hermetic.
"reproducible": false,
},
"materials": []map[string]any{sourceRef},
},
}
}
namespace App.Security;
// SLSA in-toto attestation. Emitted by the build pipeline, then signed
// with cosign and published to a transparency log.
public static class ProvenanceGenerator
{
private const string StatementType = "https://in-toto.io/Statement/v0.1";
private const string PredicateType = "https://slsa.dev/provenance/v0.2";
private const string BuildType = "https://github.com/actions/runner";
public static IReadOnlyDictionary<string, object> Generate(
string repoUrl,
string commitSha,
string builderId,
string artifactName,
string artifactHash)
{
var sourceRef = new Dictionary<string, object>
{
["uri"] = repoUrl,
["digest"] = new Dictionary<string, string> { ["sha1"] = commitSha },
};
return new Dictionary<string, object>
{
["_type"] = StatementType,
["predicateType"] = PredicateType,
["subject"] = new[]
{
new Dictionary<string, object>
{
["name"] = artifactName,
["digest"] = new Dictionary<string, string> { ["sha256"] = artifactHash },
},
},
["predicate"] = new Dictionary<string, object>
{
["builder"] = new Dictionary<string, string> { ["id"] = builderId },
["buildType"] = BuildType,
["invocation"] = new Dictionary<string, object>
{
["configSource"] = sourceRef,
},
["metadata"] = new Dictionary<string, object>
{
["buildStartedOn"] = DateTimeOffset.UtcNow.ToString("O"),
// Set to true only once the build is genuinely hermetic.
["reproducible"] = false,
},
["materials"] = new[] { sourceRef },
},
};
}
}
from datetime import datetime, timezone
from typing import Any
STATEMENT_TYPE = "https://in-toto.io/Statement/v0.1"
PREDICATE_TYPE = "https://slsa.dev/provenance/v0.2"
BUILD_TYPE = "https://github.com/actions/runner"
def generate_provenance(
repo_url: str,
commit_sha: str,
builder_id: str,
artifact_name: str,
artifact_hash: str,
) -> dict[str, Any]:
"""SLSA in-toto attestation.
Emitted by the build pipeline, then signed with cosign and published
to a transparency log.
"""
source_ref = {"uri": repo_url, "digest": {"sha1": commit_sha}}
return {
"_type": STATEMENT_TYPE,
"predicateType": PREDICATE_TYPE,
"subject": [
{"name": artifact_name, "digest": {"sha256": artifact_hash}},
],
"predicate": {
"builder": {"id": builder_id},
"buildType": BUILD_TYPE,
"invocation": {"configSource": source_ref},
"metadata": {
"buildStartedOn": datetime.now(timezone.utc).isoformat(),
# Set to True only once the build is genuinely hermetic.
"reproducible": False,
},
"materials": [source_ref],
},
}
## 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 — это не одноразовое действие. Это непрерывный процесс: сканирование при каждом билде, мониторинг новых уязвимостей, обновление зависимостей.