diff --git a/.clusterfuzzlite/Dockerfile b/.clusterfuzzlite/Dockerfile new file mode 100644 index 0000000..81d208f --- /dev/null +++ b/.clusterfuzzlite/Dockerfile @@ -0,0 +1,9 @@ +FROM rust:1.87-slim + +RUN apt-get update && apt-get install -y build-essential pkg-config && rm -rf /var/lib/apt/lists/* +RUN cargo install cargo-fuzz + +COPY . /src +WORKDIR /src + +RUN cd fuzz && cargo fuzz build 2>/dev/null || true diff --git a/.clusterfuzzlite/project.yaml b/.clusterfuzzlite/project.yaml new file mode 100644 index 0000000..64e2aa4 --- /dev/null +++ b/.clusterfuzzlite/project.yaml @@ -0,0 +1,5 @@ +language: rust +fuzzing_engines: + - libfuzzer +sanitizers: + - address diff --git a/Cargo.lock b/Cargo.lock index a15df43..2d36242 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -79,7 +79,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -251,6 +251,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -1103,6 +1105,16 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -1131,6 +1143,16 @@ version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libredox" version = "0.1.12" @@ -1259,6 +1281,14 @@ dependencies = [ "tokio", ] +[[package]] +name = "nora-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "nora-registry", +] + [[package]] name = "nora-registry" version = "0.2.31" @@ -1322,7 +1352,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a20feda..4c8c10c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "nora-registry", "nora-storage", "nora-cli", + "fuzz", ] [workspace.package] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..f894628 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nora-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +nora-registry = { path = "../nora-registry" } + +[[bin]] +name = "fuzz_validation" +path = "fuzz_targets/fuzz_validation.rs" +doc = false + +[[bin]] +name = "fuzz_docker_manifest" +path = "fuzz_targets/fuzz_docker_manifest.rs" +doc = false diff --git a/fuzz/fuzz_targets/fuzz_docker_manifest.rs b/fuzz/fuzz_targets/fuzz_docker_manifest.rs new file mode 100644 index 0000000..9a5e78d --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_docker_manifest.rs @@ -0,0 +1,8 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use nora_registry::docker_fuzz::detect_manifest_media_type; + +fuzz_target!(|data: &[u8]| { + // Fuzz Docker manifest parser — must never panic on any input + let _ = detect_manifest_media_type(data); +}); diff --git a/fuzz/fuzz_targets/fuzz_validation.rs b/fuzz/fuzz_targets/fuzz_validation.rs new file mode 100644 index 0000000..a48008e --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_validation.rs @@ -0,0 +1,13 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; +use nora_registry::validation::{ + validate_digest, validate_docker_name, validate_docker_reference, validate_storage_key, +}; + +fuzz_target!(|data: &str| { + // Fuzz all validators — they must never panic on any input + let _ = validate_storage_key(data); + let _ = validate_docker_name(data); + let _ = validate_digest(data); + let _ = validate_docker_reference(data); +}); diff --git a/nora-registry/Cargo.toml b/nora-registry/Cargo.toml index b4e0cc7..6284455 100644 --- a/nora-registry/Cargo.toml +++ b/nora-registry/Cargo.toml @@ -10,6 +10,10 @@ description = "Cloud-Native Artifact Registry - Fast, lightweight, multi-protoco keywords = ["registry", "docker", "artifacts", "cloud-native", "devops"] categories = ["command-line-utilities", "development-tools", "web-programming"] +[lib] +name = "nora_registry" +path = "src/lib.rs" + [[bin]] name = "nora" path = "src/main.rs" diff --git a/nora-registry/src/lib.rs b/nora-registry/src/lib.rs new file mode 100644 index 0000000..0bf4ae6 --- /dev/null +++ b/nora-registry/src/lib.rs @@ -0,0 +1,28 @@ +//! NORA Registry — library interface for fuzzing and testing + +pub mod validation; + +/// Re-export Docker manifest parsing for fuzz targets +pub mod docker_fuzz { + pub fn detect_manifest_media_type(data: &[u8]) -> String { + let Ok(value) = serde_json::from_slice::(data) else { + return "application/octet-stream".to_string(); + }; + if let Some(mt) = value.get("mediaType").and_then(|v| v.as_str()) { + return mt.to_string(); + } + if value.get("manifests").is_some() { + return "application/vnd.oci.image.index.v1+json".to_string(); + } + if value.get("schemaVersion").and_then(|v| v.as_i64()) == Some(2) { + if value.get("layers").is_some() { + return "application/vnd.oci.image.manifest.v1+json".to_string(); + } + return "application/vnd.docker.distribution.manifest.v2+json".to_string(); + } + if value.get("schemaVersion").and_then(|v| v.as_i64()) == Some(1) { + return "application/vnd.docker.distribution.manifest.v1+json".to_string(); + } + "application/vnd.docker.distribution.manifest.v2+json".to_string() + } +}