Shipping C++ in 2026 means more than picking the right compiler. It means a real package manager (vcpkg or Conan), a CycloneDX or SPDX SBOM emitted on every build, CVE feeds wired into CI, in-toto attestations for the build steps, and a story for “what do we do when the next OpenSSL CVE drops at 04:00 on a Sunday”. This page maps the 2026 supply-chain stack and shows the reflection pattern that closes the gap most teams accept as inevitable: the SBOM that drifts from the code.
Today
The 2026 supply-chain stack
| Layer | Tool | Notes |
|---|---|---|
| Package manager | vcpkg or Conan | Pick one and stick to it. Mixing causes pain. |
| SBOM emit | syft (source tree + container) | CycloneDX 1.5 JSON or SPDX 2.3 JSON; both interoperate downstream. |
| CVE scan | grype | Consumes SBOM + NVD + GitHub Advisory + OSV; non-zero exit on critical. |
| Build provenance | in-toto attestations + SLSA | SLSA Level 3 / 4 for the critical pipelines. |
| Secrets / signing | sigstore cosign | Sign + verify artefacts; keyless via OIDC. |
CMake recipe
# vcpkg manifest mode -- vcpkg.json next to CMakeLists.txt
include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(fmt CONFIG REQUIRED)
find_package(OpenSSL REQUIRED)
target_link_libraries(your_target PRIVATE
nlohmann_json::nlohmann_json fmt::fmt OpenSSL::SSL OpenSSL::Crypto)
CI recipe
# .github/workflows/sbom.yml -- emit + scan on every push
- run: syft . -o cyclonedx-json > sbom.cdx.json
- run: grype sbom:sbom.cdx.json --fail-on critical
- uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.cdx.json
The non-zero exit on --fail-on critical is the load-bearing line. Without it, grype is a dashboard; with it, grype is a gate.
Where conventional SBOM workflows leak
The standard pattern is: maintain a side-channel manifest (YAML) listing each vendored dep, version, license, purl. The build pipeline reads it, emits CycloneDX from it, scans with grype. The manifest drifts. A new dep gets vendored without a manifest entry. An old dep gets removed but the manifest entry stays. License changes between versions; the manifest holds the old one. Each drift is an audit-finding waiting to happen.
The next section closes the gap by encoding component metadata as P3394 annotations on the integration type. If the dep is not in the source, it cannot be in the SBOM. If you remove the integration type, the SBOM entry vanishes on the next build.
Reflection today
Encode each vendored component as a templated P3394 annotation on the integration type. A consteval walker emits the CycloneDX components fragment AND static_asserts every member carries the annotation. The SBOM cannot drift from the source because the source is the only declaration of it.
namespace sbom {
template <fixed_string Name, fixed_string Version,
fixed_string Purl, fixed_string License>
struct component {
static constexpr auto name = Name.view();
static constexpr auto version = Version.view();
static constexpr auto purl = Purl.view();
static constexpr auto license = License.view();
};
}
struct VendoredComponents {
[[=sbom::component<
"nlohmann-json", "3.11.3",
"pkg:github/nlohmann/json@v3.11.3", "MIT">{}]]
struct nlohmann_json_tag {} json;
[[=sbom::component<
"fmt", "10.2.1",
"pkg:github/fmtlib/fmt@10.2.1", "MIT">{}]]
struct fmt_tag {} fmt;
[[=sbom::component<
"openssl", "3.2.1",
"pkg:github/openssl/openssl@openssl-3.2.1", "Apache-2.0">{}]]
struct openssl_tag {} openssl;
};
emit_components<VendoredComponents>(); // emits CycloneDX 1.5 JSON
Walker output:
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": [
{ "type": "library", "bom-ref": "nlohmann-json", "name": "nlohmann-json",
"version": "3.11.3", "purl": "pkg:github/nlohmann/json@v3.11.3",
"licenses": [{ "license": { "id": "MIT" } }] },
{ "type": "library", "bom-ref": "fmt", "name": "fmt",
"version": "10.2.1", "purl": "pkg:github/fmtlib/fmt@10.2.1",
"licenses": [{ "license": { "id": "MIT" } }] },
{ "type": "library", "bom-ref": "openssl", "name": "openssl",
"version": "3.2.1", "purl": "pkg:github/openssl/openssl@openssl-3.2.1",
"licenses": [{ "license": { "id": "Apache-2.0" } }] }
]
}
Add a member without the annotation, the build refuses with the field name in the diagnostic. The pattern composes with the toolchain SBOM (syft scans the binary + system deps) — merge the in-source CycloneDX fragment with the toolchain output and you have a complete BOM that grype can consume.
Full source: posts/toolset/cpp-supply-chain-2026/examples/reflect-sbom.cpp .
Reproduce locally
docker run --rm -it \
-v "$PWD":/work -w /work \
ghcr.io/wrocpp/cpp-reflection:2026-05 \
bash -c 'clang++ -std=c++26 -freflection-latest -stdlib=libc++ -Wl,-rpath,/opt/p2996/clang/lib/aarch64-unknown-linux-gnu posts/toolset/cpp-supply-chain-2026/examples/reflect-sbom.cpp -o /tmp/h && /tmp/h'expected output
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": [
{
"type": "library",
"bom-ref": "nlohmann-json",Composable with the safety walkers
The same nonstatic_data_members_of walker that drives the SBOM emit also drives the hardened-stdlib schema lint, the qualified-compilers MISRA Rule 11.0.1 lint, the lifetime-safety borrow lint, and the SoA layout transform. One walker, five orthogonal rules. The SBOM-component-required rule is just another predicate; add a sixth, the schema enforces it.
Where this is heading
C++29 candidate features collapse the loop further:
Token injection (P3294) lets the integration type declare itself from the package manifest:
// C++29 candidate -- pseudo-syntax. As of 2026-05-15 not in any
// shipping toolchain. P3294 in WG21.
[[ inject(sbom_component) ]]
using nlohmann_json = vcpkg::dep<"nlohmann-json", "3.11.3">;
// Compiler reads vcpkg.json + injects the wrapper struct + P3394
// annotations. The integration type IS the manifest entry; the
// per-component declaration boilerplate disappears.
Profile enforcement (P3081 Sutter / P3589 Dos Reis / P3984 Stroustrup) lets a translation unit declare it imports only sbom-annotated vendored types:
// C++29 candidate.
[[ profiles::enforce(sbom_required) ]]
namespace prod_app {
// Inside this namespace: including a raw third-party header
// refuses to compile. Every external symbol must come through
// a sbom::component-annotated wrapper.
}
The 2026 story is toolchain SBOM + in-source SBOM merged at build time. C++29 collapses both into language-level enforcement.
Cross-links: the hardened-stdlib entry covers the runtime side (one CMake line, ~1000 production bugs caught). The qualified-compilers entry covers the regulated-industry context (TCL kits, MISRA C++:2023). The reflection annotations post covers P3394 in depth.
Reviewed: 2026-05-15. SBOM example verified inside cpp-reflection container; CycloneDX 1.5 JSON validates against the OWASP schema. Quarterly refresh.