Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ jobs:
IMAGE=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-${{ matrix.arch }}
podman create --name diffed-places-pipeline ${IMAGE}
podman cp \
diffed-places-pipeline:/sbom/diffed-places-pipeline.cdx.json \
artifacts/diffed-places-pipeline.cdx.json
diffed-places-pipeline:/sbom/sbom.cdx.json \
artifacts/sbom.cdx.json
podman rm diffed-places-pipeline

- name: Upload image digest and SBOM to artifacts
Expand Down Expand Up @@ -178,14 +178,14 @@ jobs:
- name: Attest amd64 SBOM
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 #v4.1.0
with:
sbom-path: artifacts-amd64/diffed-places-pipeline.cdx.json
sbom-path: artifacts-amd64/sbom.cdx.json
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.retrieve-digests.outputs.amd64_digest }}

- name: Attest arm64 SBOM
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 #v4.1.0
with:
sbom-path: artifacts-arm64/diffed-places-pipeline.cdx.json
sbom-path: artifacts-arm64/sbom.cdx.json
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.retrieve-digests.outputs.arm64_digest }}

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
*~
/target

# Generated by sbom/build_sbom.sh
/diffed-places-pipeline.cdx.json
/sbom/sbom.cdx.json

3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
anyhow = "1.0.102"
arrow = { version = "58.0.0", default-features = false }
aws-lc-rs = { version = "1.16.1", default-features = false }
clap = { version = "4.6.0", default-features = false, features = ["derive", "std"] }
deepsize = "0.2.0"
deepsize_derive = "0.1.2"
Expand All @@ -26,11 +27,13 @@ protobuf_iter = { version = "0.1.3", default-features = false }
rayon = "1.11.0"
reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls", "stream"] }
rmp-serde = { version = "1.3.1", default-features = false }
rustls = { version = "0.23.37", default-features = false, features = ["aws_lc_rs"] }
s2 = "0.0.13"
serde = "1.0.228"
serde_json = "1.0.148"
tempfile = "3.27.0"
tokio = { version = "1.50.0", default-features = false }
webpki-roots = "1.0.6"

[dev-dependencies]
assert_cmd = "2.2.0"
Expand Down
24 changes: 14 additions & 10 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ FROM rust:1.92.0-alpine3.23 AS builder
WORKDIR /usr/diffed-places

COPY Cargo.toml Cargo.lock .
COPY sbom sbom
COPY src src
COPY tests tests

RUN apk add --no-cache ca-certificates
RUN cargo install cargo-cyclonedx
RUN cargo build --release
RUN cargo test --release
RUN cargo cyclonedx --no-build-deps --format json --spec-version=1.5
RUN cargo build --release --locked
RUN cargo test --release --locked

# TODO: Remove this once Alpine 3.24 has been released.
RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories \
&& apk update \
&& apk add --no-cache cargo-cyclonedx@edge
RUN apk add --no-cache jq
RUN sh sbom/build_sbom.sh


# ----------------------------------------------------------------------------
Expand All @@ -38,14 +43,13 @@ ARG BUILD_TIMESTAMP
ARG VCS_REF
ARG VCS_URL

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

COPY --from=builder --chown=1000:1000 \
/usr/diffed-places/target/release/diffed-places-pipeline \
/app/diffed-places-pipeline

COPY --from=builder --chown=1000:1000 \
/usr/diffed-places/diffed-places-pipeline.cdx.json \
/sbom/diffed-places-pipeline.cdx.json
/usr/diffed-places/sbom/sbom.cdx.json \
/sbom/sbom.cdx.json

USER 1000

Expand All @@ -57,6 +61,6 @@ LABEL \
org.opencontainers.image.description="Data pipeline for Diffed Places" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.sbom="/sbom/diffed-places-pipeline.cdx.json" \
org.opencontainers.image.sbom="/sbom/sbom.cdx.json" \
org.opencontainers.image.source=$VCS_URL \
org.opencontainers.image.vendor="Sascha Brawer <sascha@brawer.ch>"
37 changes: 37 additions & 0 deletions sbom/build_sbom.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Build a Software Bill Of Materials (SBOM) in CycloneDX 1.6 JSON format.
#
# As of March 2026, the "cargo cyclonedx" tool cannot auto-detect what
# TLS version, crypto algorithms and root certificates we're using,
# so we embed a manually crafted CBOM into the auto-generated SBOM.

SBOM_IN="diffed-places-pipeline.cdx.json"
CBOM_PATCH="sbom/cbom.cdx.json"
SBOM_OUT="sbom/sbom.cdx.json"

cargo cyclonedx --no-build-deps --format json --spec-version=1.5

# Extract exact resolved version from Cargo.lock via cargo metadata
WEBPKI_VERSION=$(cargo metadata --format-version 1 --locked |
jq -r '.packages[] | select(.name == "webpki-roots") | .version')

echo "webpki-roots version: $WEBPKI_VERSION"

jq \
--slurpfile cbom "$CBOM_PATCH" \
--arg webpki_version "$WEBPKI_VERSION" \
'
.specVersion = "1.6" |
.metadata.supplier = {
"name": "Diffed Places Project",
"url": ["https://github.com/diffed-places/"]
} |
.components += ($cbom[0].components | map(
if .name == "Mozilla Root CA Bundle" then
.version = $webpki_version |
.purl = "pkg:cargo/webpki-roots@\($webpki_version)" |
.externalReferences[0].url = "https://crates.io/crates/webpki-roots/\($webpki_version)"
else . end
)) |
.dependencies += ($cbom[0].dependencies // [])
' \
"$SBOM_IN" > "$SBOM_OUT"
50 changes: 50 additions & 0 deletions sbom/cbom.cdx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"components": [
{
"type": "cryptographic-asset",
"bom-ref": "tls-1.3-protocol",
"name": "TLS",
"purl": "pkg:generic/tls@1.3",
"version": "1.3",
"cryptoProperties": {
"assetType": "protocol",
"protocolProperties": {
"type": "tls",
"version": "1.3",
"cipherSuites": [
{ "name": "TLS_AES_256_GCM_SHA384" },
{ "name": "TLS_AES_128_GCM_SHA256" },
{ "name": "TLS_CHACHA20_POLY1305_SHA256" }
]
}
}
},
{
"type": "cryptographic-asset",
"bom-ref": "webpki-roots-ca-bundle",
"name": "Mozilla Root CA Bundle",
"cryptoProperties": {
"assetType": "related-crypto-material",
"relatedCryptoMaterialProperties": {
"type": "trust-anchor-table"
}
},
"externalReferences": [
{
"type": "distribution",
"url": "https://crates.io/crates/webpki-roots",
"comment": "Embedded at compile time via the webpki-roots crate"
}
]
}
],
"dependencies": [
{
"ref": "your-app-bom-ref",
"dependsOn": [
"tls-1.3-protocol",
"webpki-roots-ca-bundle"
]
}
]
}
10 changes: 6 additions & 4 deletions src/atp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ use crate::place::{ParquetWriter, Place};

mod fetch;

pub async fn import_atp(progress: &MultiProgress, workdir: &Path) -> Result<PathBuf> {
pub async fn import_atp(
client: &Client,
progress: &MultiProgress,
workdir: &Path,
) -> Result<PathBuf> {
let out = workdir.join("alltheplaces.parquet");
if out.exists() {
return Ok(out);
}

let client = Client::new();
let input_zip =
fetch::fetch_atp(fetch::ATP_RUN_HISTORY_URL, &client, progress, workdir).await?;
let input_zip = fetch::fetch_atp(fetch::ATP_RUN_HISTORY_URL, client, progress, workdir).await?;

// To avoid deadlock, we must not use Rayon threads here.
// https://dev.to/sgchris/scoped-threads-with-stdthreadscope-in-rust-163-48f9
Expand Down
85 changes: 84 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ use std::path::PathBuf;
use anyhow::{Result, anyhow};
use clap::{Parser, Subcommand};
use indicatif::MultiProgress;
use reqwest::Client;
use rustls::version::TLS13;
use rustls::{ClientConfig, RootCertStore};
use std::fs::create_dir;
use webpki_roots::TLS_SERVER_ROOTS;

use diffed_places_pipeline::{build_coverage, import_atp, import_osm};

Expand All @@ -25,8 +29,11 @@ enum Commands {
fn main() -> Result<()> {
let args = Cli::parse();
env_logger::init();
init_crypto();

match &args.command {
Some(Commands::Run { workdir }) => {
let client = build_client();
let progress = MultiProgress::new();
if !workdir.exists() {
create_dir(workdir)?;
Expand All @@ -35,7 +42,7 @@ fn main() -> Result<()> {
let atp = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?
.block_on(import_atp(&progress, workdir))?;
.block_on(import_atp(&client, &progress, workdir))?;

let coverage = build_coverage(&atp, &progress, workdir)?;
import_osm(&coverage, &progress, workdir)?;
Expand All @@ -44,3 +51,79 @@ fn main() -> Result<()> {
None => Err(anyhow!("no subcommand given")),
}
}

fn init_crypto() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
}

fn build_client() -> reqwest::Client {
let mut root_store = RootCertStore::empty();
root_store.extend(TLS_SERVER_ROOTS.iter().cloned());

// Install the crypto provider as process-wide default,
// which makes librqbit use it as well for BitTorrent.
let provider = rustls::crypto::aws_lc_rs::default_provider();

let config = ClientConfig::builder_with_provider(provider.into())
.with_protocol_versions(&[&TLS13])
.unwrap()
.with_root_certificates(root_store)
.with_no_client_auth();

Client::builder()
.use_preconfigured_tls(config)
.build()
.unwrap()
}

#[cfg(test)]
mod tests {
use super::*;
use std::sync::OnceLock;

static INIT_CRYPTO: OnceLock<()> = OnceLock::new();

fn init_crypto_once() {
INIT_CRYPTO.get_or_init(|| {
init_crypto();
});
}

#[test]
fn test_build_client_constructs_successfully() {
init_crypto_once();

// Verifies the TLS config and client build without panicking.
// reqwest::Client doesn't expose internals that we could test
// without sending traffic to external services over the network.
let _ = build_client();
}

#[tokio::test]
#[ignore = "requires network"]
async fn test_client_rejects_tls12() {
init_crypto_once();

let client = build_client();
// badssl.com provides test endpoints for various TLS scenarios
let result = client.get("https://tls-v1-2.badssl.com:1012/").send().await;
assert!(
result.is_err(),
"Expected TLS 1.2 connection to be rejected"
);
}

#[tokio::test]
#[ignore = "requires network"]
async fn test_client_accepts_tls13() {
init_crypto_once();

let client = build_client();
let result = client.get("https://tls13.akamai.io/").send().await;
assert!(
result.is_ok(),
"Expected TLS 1.3 connection to succeed: {:?}",
result.err()
);
}
}
Loading