diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bc1b93d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main, dev ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + rust: [stable, nightly] + + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --verbose --all + + - name: Run tests + run: cargo test --verbose --all + + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + if: matrix.rust == 'stable' + + - name: Check formatting + run: cargo fmt --all -- --check + if: matrix.rust == 'stable' + + build-windows: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v3 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v3 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v3 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Build + run: cargo build --verbose --all + + - name: Run tests + run: cargo test --verbose --all diff --git a/.gitignore b/.gitignore index 66549a6..8c63ce0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Rust build output target/ -.github/ .DS_Store/ .vscode/ out/ diff --git a/SECURITY.md b/SECURITY.md index d3b64a9..a9deefe 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ Grob is currently in active development (Alpha phase). Security updates and patc | Version | Status | Supported | | ------- | ------ | --------- | -| 0.0.1 | Initial Release (Alpha) | ✅ Full Support | +| 0.0.1 | Initial Release (Alpha) | Full Support | **Note**: Grob is at its initial release (0.0.1). As the project develops, new versions will be released with new features, improvements, and security patches. @@ -24,28 +24,28 @@ Grob is currently in active development (Alpha phase). Security updates and patc Grob is in **Alpha development** and should **not be used in production environments** without thorough security review and testing. The following security features are either incomplete or planned: #### Implemented Security Features -- ✅ Memory safety via Rust's ownership system -- ✅ Safe concurrency primitives -- ✅ Input validation in parsers (HTML/CSS) -- ✅ Basic error handling and recovery +- Memory safety via Rust's ownership system +- Safe concurrency primitives +- Input validation in parsers (HTML/CSS) +- Basic error handling and recovery #### Planned Security Features -- 🚧 HTTPS/TLS support (currently HTTP only) -- 🚧 Content Security Policy (CSP) enforcement -- 🚧 CORS (Cross-Origin Resource Sharing) support -- 🚧 XSS (Cross-Site Scripting) protection -- 🚧 CSRF (Cross-Site Request Forgery) tokens -- 🚧 Secure cookie handling -- 🚧 Sandbox/isolation for JavaScript execution -- 🚧 Safe resource loading with origin verification +- HTTPS/TLS support (currently HTTP only) +- Content Security Policy (CSP) enforcement +- CORS (Cross-Origin Resource Sharing) support +- XSS (Cross-Site Scripting) protection +- CSRF (Cross-Site Request Forgery) tokens +- Secure cookie handling +- Sandbox/isolation for JavaScript execution +- Safe resource loading with origin verification #### Known Security Limitations -- ⚠️ No HTTPS support - all connections are unencrypted HTTP -- ⚠️ Limited input validation for malicious content -- ⚠️ JavaScript execution not sandboxed (in development) -- ⚠️ No authentication or authorization framework -- ⚠️ File system access not restricted -- ⚠️ No protection against malicious stylesheets or scripts +- No HTTPS support - all connections are unencrypted HTTP +- Limited input validation for malicious content +- JavaScript execution not sandboxed (in development) +- No authentication or authorization framework +- File system access not restricted +- No protection against malicious stylesheets or scripts --- @@ -128,25 +128,37 @@ Include the following information: ## Security Roadmap -### Phase 1 (Current - Alpha) +### Phase 1 (Prealpha - Current) - Focus: Core engine stability and correctness - Security: Basic input validation, memory safety +- Internal testing only - Target: Q1 2026 -### Phase 2 (Beta) -- HTTPS/TLS support -- Basic sandbox for JavaScript -- Content Security Policy support +### Phase 2 (Alpha) +- Initial HTTPS/TLS implementation +- Early JavaScript sandbox prototype +- Basic same-origin policy enforcement +- Initial Content Security Policy parsing +- Fuzz testing for parser and rendering engine - Target: Q2 2026 -### Phase 3 (1.0 Release) +### Phase 3 (Beta) +- Full HTTPS/TLS support +- Stable JavaScript sandbox +- Content Security Policy enforcement +- Initial CORS support +- Security bug bounty program +- Target: Q3 2026 + +### Phase 4 (1.0 Release) - Full CORS implementation - Comprehensive XSS protection - Secure cookie handling +- Hardened sandboxing - Security audit by external firm - Target: Q4 2026 -### Phase 4 (Post 1.0) +### Phase 5 (Post 1.0) - Advanced sandboxing - Service Worker security - Enhanced authentication support @@ -189,7 +201,50 @@ Grob aims to comply with: For security matters only (not general support): - Email: **elyas@albahrani.org** -- PGP Key: [To be added when available] +- PGP Key: + +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +GULOdPTuuycMtNiuuXGvvwJ3Ew== +=yVD+ +-----END PGP PUBLIC KEY BLOCK----- + For general support and bug reports: - GitHub Issues: https://github.com/elyas-code/grob/issues @@ -212,6 +267,6 @@ We thank all security researchers and community members who responsibly report v --- -**Last Updated**: January 22, 2026 +**Last Updated**: March 6, 2026 -**Status**: Alpha - Security features still under development +**Status**: Prealpha - Security features still under development diff --git a/engine/tests/image_support_tests.rs b/engine/tests/feature_support_tests.rs similarity index 80% rename from engine/tests/image_support_tests.rs rename to engine/tests/feature_support_tests.rs index 221e291..22507f4 100644 --- a/engine/tests/image_support_tests.rs +++ b/engine/tests/feature_support_tests.rs @@ -14,94 +14,94 @@ mod url_resolution_tests { #[test] fn test_parse_simple_url() { - let url = ParsedUrl::parse("https://example.com/path/to/file.html").unwrap(); + let url = ParsedUrl::parse("https://en.wikipedia.org/wiki/Rust_(programming_language)").unwrap(); assert_eq!(url.scheme, "https"); - assert_eq!(url.host, "example.com"); - assert_eq!(url.path, "/path/to/file.html"); + assert_eq!(url.host, "en.wikipedia.org"); + assert_eq!(url.path, "/wiki/Rust_(programming_language)"); assert_eq!(url.port, None); } #[test] fn test_parse_url_with_port() { - let url = ParsedUrl::parse("http://localhost:8080/api/data").unwrap(); - assert_eq!(url.scheme, "http"); - assert_eq!(url.host, "localhost"); - assert_eq!(url.port, Some(8080)); - assert_eq!(url.path, "/api/data"); + let url = ParsedUrl::parse("https://github.com:443/torvalds/linux").unwrap(); + assert_eq!(url.scheme, "https"); + assert_eq!(url.host, "github.com"); + assert_eq!(url.port, Some(443)); + assert_eq!(url.path, "/torvalds/linux"); } #[test] fn test_parse_url_with_query_and_fragment() { - let url = ParsedUrl::parse("https://example.com/search?q=test&page=1#results").unwrap(); + let url = ParsedUrl::parse("https://www.google.com/search?q=rust+programming&page=1#results").unwrap(); assert_eq!(url.path, "/search"); - assert_eq!(url.query, Some("q=test&page=1".to_string())); + assert_eq!(url.query, Some("q=rust+programming&page=1".to_string())); assert_eq!(url.fragment, Some("results".to_string())); } #[test] fn test_resolve_relative_same_directory() { - let result = resolve_url("https://example.com/path/page.html", "image.png"); - assert_eq!(result, "https://example.com/path/image.png"); + let result = resolve_url("https://en.wikipedia.org/wiki/Rust/page.html", "logo.png"); + assert_eq!(result, "https://en.wikipedia.org/wiki/Rust/logo.png"); } #[test] fn test_resolve_relative_parent_directory() { - let result = resolve_url("https://example.com/a/b/c.html", "../img.png"); - assert_eq!(result, "https://example.com/a/img.png"); + let result = resolve_url("https://github.com/rust-lang/rust/docs/guide.html", "../images/logo.png"); + assert_eq!(result, "https://github.com/rust-lang/images/logo.png"); } #[test] fn test_resolve_relative_root() { - let result = resolve_url("https://example.com/deep/nested/page.html", "/assets/image.png"); - assert_eq!(result, "https://example.com/assets/image.png"); + let result = resolve_url("https://github.com/deep/nested/page.html", "/assets/banner.png"); + assert_eq!(result, "https://github.com/assets/banner.png"); } #[test] fn test_resolve_protocol_relative() { - let result = resolve_url("https://example.com/page.html", "//cdn.example.com/lib.js"); - assert_eq!(result, "https://cdn.example.com/lib.js"); + let result = resolve_url("https://github.com/page.html", "//cdn.jsdelivr.net/npm/library@latest/dist/lib.js"); + assert_eq!(result, "https://cdn.jsdelivr.net/npm/library@latest/dist/lib.js"); } #[test] fn test_resolve_absolute_url_unchanged() { - let result = resolve_url("https://example.com/page.html", "https://other.com/image.png"); - assert_eq!(result, "https://other.com/image.png"); + let result = resolve_url("https://github.com/page.html", "https://raw.githubusercontent.com/torvalds/linux/master/README"); + assert_eq!(result, "https://raw.githubusercontent.com/torvalds/linux/master/README"); } #[test] fn test_resolve_with_base_href() { let result = resolve_url_with_base( - "https://example.com/app/index.html", + "https://github.com/app/index.html", Some("/static/"), "logo.png" ); - assert_eq!(result, "https://example.com/static/logo.png"); + assert_eq!(result, "https://github.com/static/logo.png"); } #[test] fn test_resolve_with_absolute_base_href() { let result = resolve_url_with_base( - "https://example.com/app/index.html", - Some("https://cdn.example.com/assets/"), + "https://github.com/app/index.html", + Some("https://cdn.jsdelivr.net/npm/assets/"), "image.jpg" ); - assert_eq!(result, "https://cdn.example.com/assets/image.jpg"); + assert_eq!(result, "https://cdn.jsdelivr.net/npm/assets/image.jpg"); } #[test] fn test_resolve_without_base_href() { let result = resolve_url_with_base( - "https://example.com/app/index.html", + "https://github.com/app/index.html", None, "local.png" ); - assert_eq!(result, "https://example.com/app/local.png"); + assert_eq!(result, "https://github.com/app/local.png"); } #[test] fn test_data_uri_unchanged() { - let data_uri = "data:image/png;base64,iVBORw0KGgo="; - let result = resolve_url("https://example.com/page.html", data_uri); + let data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; + let result = resolve_url("https://github.com/page.html", data_uri); assert_eq!(result, data_uri); } } @@ -402,9 +402,9 @@ mod cache_tests { let data = b"image data".to_vec(); let headers = CacheHeaders::default(); - cache.store("https://example.com/img.png", data.clone(), "image/png".to_string(), headers); + cache.store("https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/img/logo.png", data.clone(), "image/png".to_string(), headers); - match cache.lookup("https://example.com/img.png") { + match cache.lookup("https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/img/logo.png") { CacheLookup::Hit(entry) => { assert_eq!(entry.data, data); assert_eq!(entry.content_type, "image/png"); @@ -417,7 +417,7 @@ mod cache_tests { fn test_cache_miss() { let cache = AssetCache::new(); - match cache.lookup("https://example.com/nonexistent.png") { + match cache.lookup("https://cdn.jsdelivr.net/npm/nonexistent/image.png") { CacheLookup::Miss => {} _ => panic!("Expected cache miss"), } @@ -432,9 +432,9 @@ mod cache_tests { ..Default::default() }; - cache.store("https://example.com/private.png", vec![1, 2, 3], "image/png".to_string(), headers); + cache.store("https://github.com/api/private/token", vec![1, 2, 3], "image/png".to_string(), headers); - match cache.lookup("https://example.com/private.png") { + match cache.lookup("https://github.com/api/private/token") { CacheLookup::Miss => {} _ => panic!("Expected miss for no-store"), } @@ -449,9 +449,9 @@ mod cache_tests { ..Default::default() }; - cache.store("https://example.com/img.png", vec![1, 2, 3], "image/png".to_string(), headers); + cache.store("https://cdn.jsdelivr.net/npm/library@1.0.0/dist/img.png", vec![1, 2, 3], "image/png".to_string(), headers); - match cache.lookup("https://example.com/img.png") { + match cache.lookup("https://cdn.jsdelivr.net/npm/library@1.0.0/dist/img.png") { CacheLookup::Hit(entry) => { assert_eq!(entry.etag, Some("\"abc123\"".to_string())); } @@ -474,12 +474,12 @@ mod cache_tests { let cache = AssetCache::new(); let headers = CacheHeaders::default(); - cache.store("https://example.com/img.png", vec![1, 2, 3], "image/png".to_string(), headers); + cache.store("https://cdn.jsdelivr.net/npm/library/img.png", vec![1, 2, 3], "image/png".to_string(), headers); // Refresh should update the cached_at time - cache.refresh("https://example.com/img.png"); + cache.refresh("https://cdn.jsdelivr.net/npm/library/img.png"); - match cache.lookup("https://example.com/img.png") { + match cache.lookup("https://cdn.jsdelivr.net/npm/library/img.png") { CacheLookup::Hit(_) => {} _ => panic!("Expected hit after refresh"), } @@ -490,8 +490,8 @@ mod cache_tests { let cache = AssetCache::new(); let headers = CacheHeaders::default(); - cache.store("https://example.com/a.png", vec![1, 2, 3, 4, 5], "image/png".to_string(), headers.clone()); - cache.store("https://example.com/b.png", vec![1, 2, 3], "image/png".to_string(), headers); + cache.store("https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/img/a.png", vec![1, 2, 3, 4, 5], "image/png".to_string(), headers.clone()); + cache.store("https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/img/b.png", vec![1, 2, 3], "image/png".to_string(), headers); let stats = cache.stats(); assert_eq!(stats.entry_count, 2); @@ -528,18 +528,18 @@ mod html_extraction_tests { #[test] fn test_extract_link_icon() { - let html = r#"
"#; + let html = r#""#; let dom = HtmlParser::new(html).parse(); let refs = extract_image_refs(&dom); assert_eq!(refs.len(), 1); - assert_eq!(refs[0].url, "favicon.ico"); + assert_eq!(refs[0].url, "https://github.com/favicon.ico"); assert!(matches!(refs[0].ref_type, ImageRefType::Favicon)); } #[test] fn test_extract_apple_touch_icon() { - let html = r#""#; + let html = r#""#; let dom = HtmlParser::new(html).parse(); let refs = extract_image_refs(&dom); @@ -549,11 +549,11 @@ mod html_extraction_tests { #[test] fn test_extract_base_href() { - let html = r#"